#!/usr/bin/env lua
-- Example lmcp server — shell tools
-- Usage: lua example_server.lua [port]

local dir = arg[0]:match('(.*/)')  or './'
package.path = package.path .. ';' .. dir .. '?.lua'

local lmcp = require('lmcp')

local server = lmcp.new("example-tools", {
    port = tonumber(arg[1]) or 8080,
})

-- The optional 5th `opts` arg to server:tool carries MCP annotations.
-- Omit it and clients assume the worst (destructive, openWorld) — fine
-- for prototypes; declare annotations once you know each tool's stance.

server:tool("shell", "Execute a shell command", {
    type = "object",
    properties = {
        command = { type = "string", description = "Shell command to execute" },
        timeout = { type = "integer", description = "Timeout in seconds", default = 30 },
    },
    required = { "command" },
}, function(args)
    local handle = io.popen(args.command .. ' 2>&1', 'r')
    if not handle then return "Error: could not execute command" end
    local result = handle:read('*a')
    handle:close()
    return result ~= '' and result or '(no output)'
end, {
    annotations = {
        title           = "Run shell",
        readOnlyHint    = false,
        destructiveHint = true,
        idempotentHint  = false,
        openWorldHint   = true,
    },
})

server:tool("read_file", "Read a file", {
    type = "object",
    properties = {
        path = { type = "string", description = "File path to read" },
    },
    required = { "path" },
}, function(args)
    local f = io.open(args.path, 'r')
    if not f then return "Error: could not open " .. args.path end
    local content = f:read('*a')
    f:close()
    return content
end, {
    annotations = {
        title           = "Read file",
        readOnlyHint    = true,
        destructiveHint = false,
        idempotentHint  = true,
        openWorldHint   = false,
    },
})

server:tool("write_file", "Write content to a file", {
    type = "object",
    properties = {
        path = { type = "string", description = "File path to write" },
        content = { type = "string", description = "Content to write" },
    },
    required = { "path", "content" },
}, function(args)
    local f = io.open(args.path, 'w')
    if not f then return "Error: could not open " .. args.path .. " for writing" end
    f:write(args.content)
    f:close()
    return string.format("Written %d bytes to %s", #args.content, args.path)
end, {
    annotations = {
        title           = "Write file",
        readOnlyHint    = false,
        destructiveHint = true,
        idempotentHint  = true,
        openWorldHint   = false,
    },
})

server:tool("list_dir", "List directory contents", {
    type = "object",
    properties = {
        path = { type = "string", description = "Directory path", default = "." },
    },
}, function(args)
    local path = args.path or '.'
    local handle = io.popen('ls -1 ' .. path:gsub("'", "'\\''") .. ' 2>&1')
    if not handle then return "Error: could not list " .. path end
    local result = handle:read('*a')
    handle:close()
    return result
end, {
    annotations = {
        title           = "List directory",
        readOnlyHint    = true,
        destructiveHint = false,
        idempotentHint  = true,
        openWorldHint   = false,
    },
})

-- ---- Resources (MCP primitive — see issue #5) ----
-- Tools-only servers force the client to spend a tools/call round-trip
-- for every read. Resources let the client list and read by URI, with a
-- stable identity it can cache and reference in prompts.

server:resource("text://greeting", {
    name = "Greeting",
    mimeType = "text/plain",
}, function() return "Hello from lmcp!" end)

-- Tiny binary resource: 8-byte PNG signature, demonstrates blob handling.
server:resource("data://lmcp.png", {
    name = "PNG signature",
    mimeType = "image/png",
}, function()
    return { blob_bytes = "\x89PNG\r\n\x1a\n", mimeType = "image/png" }
end)

-- Template: any local file. `args.path` is captured greedily (no leading
-- slash because the template literal already includes ///).
server:resource_template("file:///{path}", {
    name = "Local file",
    mimeType = "text/plain",
}, function(args)
    local f = io.open("/" .. args.path, "r")
    if not f then error("file not found: /" .. args.path) end
    local content = f:read("*a"); f:close()
    return content
end)

-- ---- Prompts (MCP primitive — see issue #6) ----
-- Parameterised prompt templates the client surfaces as a menu
-- (slash-commands, snippets). Handler returns either a plain string (one
-- user-role text message) or a full { description?, messages = {...} }
-- shape for finer control.

server:prompt("release_note", {
    description = "Draft a release note for a given version",
    arguments = {
        { name = "version", description = "Tag, e.g. v0.7.1", required = true },
        { name = "since",   description = "Previous tag",      required = false },
    },
}, function(args)
    return "Write concise release notes for version " .. (args.version or "?")
        .. " since " .. (args.since or "the previous tag")
        .. ". Group by category (features / fixes / docs)."
end)

-- Completion for the release_note prompt's `version` argument. Returned
-- list is filtered against `value` (prefix match) by the server's spec
-- contract is "candidates"; clients may further filter.
server:complete("ref/prompt", "release_note", "version", function(value, ctx)
    local all = { "v0.5.0", "v0.5.1", "v0.5.2", "v0.5.3", "v0.5.4",
                  "v0.6.0", "v0.7.0", "v0.7.1", "v1.0.0-rc1" }
    if value == "" then return all end
    local out = {}
    for _, v in ipairs(all) do
        if v:sub(1, #value) == value then out[#out + 1] = v end
    end
    return out
end)

local transport = os.getenv("LMCP_TRANSPORT") or "http"
if transport == "stdio" then
    if os.getenv("LMCP_PORT") then
        io.stderr:write("lmcp: LMCP_PORT ignored in stdio mode\n")
    end
    server:run_stdio()
else
    io.stderr:write("Starting lmcp example server...\n")
    server:run()
end
