#!/usr/bin/env node /** * Posimai Lab MCP Server * * Provides File System and Gitea integration for the AI Agent. * Communication: Stdio (Standard Input/Output) * Scope: * - /app (Server code) * - /data/gitea_files (Gitea Repo Data) */ 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 { z } from "zod"; import fs from "fs/promises"; import path from "path"; // Define allowed scopes for security const ALLOWED_Root = "/data/gitea_files"; const APP_Root = "/app"; // Helper to validate paths function validatePath(requestedPath) { const resolved = path.resolve(requestedPath); if (resolved.startsWith(ALLOWED_Root) || resolved.startsWith(APP_Root)) { return resolved; } throw new Error(`Access denied: Path ${resolved} is outside allowed scopes.`); } // Create Server Instance const server = new Server( { name: "posimai-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); /** * Tool Definitions */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_files", description: "List files in a directory within the allowed scope.", inputSchema: zodToJsonSchema( z.object({ path: z.string().describe("Directory path to list (e.g., /data/gitea_files)"), }) ), }, { name: "read_file", description: "Read the content of a text file.", inputSchema: zodToJsonSchema( z.object({ path: z.string().describe("File path to read"), }) ), }, { name: "write_file", description: "Write content to a file (overwrite).", inputSchema: zodToJsonSchema( z.object({ path: z.string().describe("File path to write"), content: z.string().describe("Content to write"), }) ), }, { name: "hello_world", description: "Simple test tool to verify connection.", inputSchema: zodToJsonSchema(z.object({})), }, ], }; }); /** * Tool Execution Handler */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "hello_world": return { content: [ { type: "text", text: "Hello from Synology! MCP Server is running successfully. 🚀", }, ], }; case "list_files": { const dirPath = validatePath(String(args.path)); const files = await fs.readdir(dirPath, { withFileTypes: true }); const listing = files.map((f) => `${f.isDirectory() ? "[DIR]" : "[FILE]"} ${f.name}` ).join("\n"); return { content: [{ type: "text", text: listing || "(Empty Directory)" }], }; } case "read_file": { const filePath = validatePath(String(args.path)); const content = await fs.readFile(filePath, "utf-8"); return { content: [{ type: "text", text: content }], }; } case "write_file": { const filePath = validatePath(String(args.path)); await fs.writeFile(filePath, String(args.content), "utf-8"); return { content: [{ type: "text", text: `Successfully wrote to ${filePath}` }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Start Server async function run() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server running on stdio"); } run().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); }); // --- Helper for Zod to JSON Schema (Simplified) --- function zodToJsonSchema(zodObj) { // In a real app we'd use 'zod-to-json-schema' package, // but for simplicity/dependencies we'll do basic mapping here or use the loose definition. // Actually, let's keep it simple for now as the strict schema isn't strictly enforced by the starter client yet, // but better to specific properties. // Quick-and-dirty conversion for the simple objects we use above if (zodObj._def.typeName === 'ZodObject') { const properties = {}; const required = []; for (const [key, schema] of Object.entries(zodObj.shape)) { properties[key] = { type: 'string', description: schema.description }; if (!schema.isOptional()) required.push(key); } return { type: 'object', properties, required }; } return { type: 'object', properties: {} }; }