import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { readFileSync } from 'node:fs'; import { homedir, hostname } from 'node:os'; import { getPaths, loadAuth } from './lib/config.js'; import { resolveSourceProject } from './lib/domain-detector.js'; import { makeGiteaBackend } from './lib/gitea.js'; import { makeTasksTools } from './tools/tasks.js'; import { makeKnowledgeTools } from './tools/knowledge.js'; import { makeMetaTools } from './tools/meta.js'; import { makeWorkersTools } from './tools/workers.js'; import { makeOpsTools } from './tools/ops.js'; function getCwdArg(): string { const i = process.argv.indexOf('--cwd'); if (i >= 0 && process.argv[i + 1]) return process.argv[i + 1]; return process.cwd(); } // Resolves to package.json regardless of run context: dist/server.js → ../package.json, // or src/server.ts (tests / ts-node) → ../package.json. Keeps the MCP server-info // version field in sync with package.json without a hardcoded literal. function readPackageVersion(): string { const pkgUrl = new URL('../package.json', import.meta.url); const raw = readFileSync(pkgUrl, 'utf8'); const pkg = JSON.parse(raw) as { version?: unknown }; if (typeof pkg.version !== 'string' || pkg.version.length === 0) { throw new Error(`package.json at ${pkgUrl.pathname} missing string "version" field`); } return pkg.version; } async function main(): Promise { const paths = getPaths(homedir()); const cwd = getCwdArg(); // Auth + backend are optional for read-only operation. Write tools surface // a clear error if these aren't available. const auth = await loadAuth(paths.authFile).catch(() => null); const backend = auth ? makeGiteaBackend({ baseUrl: auth.giteaUrl, token: auth.giteaToken }) : undefined; const { sourceProject, fellBack } = await resolveSourceProject(cwd); if (fellBack) { console.error( `[projects-meta] warning: cwd=${cwd} has no parseable git remote — ` + `identity-footer will use bare basename "${sourceProject}" instead of ` + `qualified /. Tasks/wiki created from here will not record their source repo.`, ); } const tools = [ ...makeTasksTools({ cacheFile: paths.cacheFile, backend, giteaUser: auth?.giteaUser, agendaTasksRepo: auth?.agendaTasksRepo, agendaTasksOwner: auth?.agendaTasksOwner, aggregateSkipOwners: auth?.giteaAggregateSkipOwners, machine: hostname(), sourceProject, }), ...makeKnowledgeTools({ wikiRoot: paths.sharedWikiClone, projectCwd: cwd, cacheFile: paths.cacheFile, backend, giteaUser: auth?.giteaUser, projectsWikiRepo: auth?.projectsWikiRepo, machine: hostname(), sourceProject, }), ...makeMetaTools({ paths }), ...makeWorkersTools(), ...makeOpsTools({ cacheFile: paths.cacheFile }), ]; const byName = new Map(); for (const t of tools) { if (byName.has(t.name)) throw new Error(`Duplicate tool name: ${t.name}`); byName.set(t.name, t); } const server = new Server( { name: 'projects-meta-mcp', version: readPackageVersion() }, { capabilities: { tools: {} } }, ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, })), })); server.setRequestHandler(CallToolRequestSchema, async (req) => { const tool = byName.get(req.params.name); if (!tool) { return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true, }; } return tool.handler(req.params.arguments ?? {}); }); const transport = new StdioServerTransport(); await server.connect(transport); } await main();