ponshu-room-lite/tools/synology/mcp/index.js

185 lines
5.0 KiB
JavaScript

#!/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: {} };
}