diff --git a/package.json b/package.json index 10e4d5a..d3e466f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "dist/index.js", "scripts": { "dev": "tsx watch --inspect src/index.ts", + "dev:stdio": "tsx src/stdio.ts", "dev:internal": "AUTH_MODE=internal tsx watch --inspect src/index.ts", "dev:external": "concurrently \"AUTH_MODE=auth_server PORT=3001 BASE_URI=http://localhost:3001 tsx watch src/index.ts\" \"sleep 3 && AUTH_MODE=external AUTH_SERVER_URL=http://localhost:3001 tsx watch --inspect src/index.ts\"", "build": "tsc && npm run copy-static && npm run build:apps", diff --git a/scripts/test-stdio.ts b/scripts/test-stdio.ts new file mode 100644 index 0000000..487db83 --- /dev/null +++ b/scripts/test-stdio.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env npx tsx +/** + * Test script to verify the stdio server works correctly. + * Connects to the server, lists resources, and reads the MCP App resource. + */ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +async function main() { + console.log("Starting stdio client test...\n"); + + const transport = new StdioClientTransport({ + command: "npx", + args: ["tsx", "src/stdio.ts"], + }); + + const client = new Client({ + name: "test-client", + version: "1.0.0", + }); + + await client.connect(transport); + console.log("Connected to server\n"); + + // Initialize + const serverInfo = client.getServerVersion(); + console.log("Server info:", serverInfo); + console.log(); + + // List resources + console.log("Listing resources..."); + const resources = await client.listResources(); + console.log(`Found ${resources.resources.length} resources:`); + for (const resource of resources.resources) { + console.log(` - ${resource.name} (${resource.uri})`); + } + console.log(); + + // Find and read the MCP App resource + const mcpAppResource = resources.resources.find((r) => + r.uri.startsWith("ui://") + ); + if (mcpAppResource) { + console.log(`Reading MCP App resource: ${mcpAppResource.uri}`); + const content = await client.readResource({ uri: mcpAppResource.uri }); + const textContent = content.contents[0]; + if ("text" in textContent) { + console.log(` MIME type: ${textContent.mimeType}`); + console.log(` Content length: ${textContent.text.length} bytes`); + console.log(` First 200 chars: ${textContent.text.slice(0, 200)}...`); + } + console.log(); + } + + // List tools + console.log("Listing tools..."); + const tools = await client.listTools(); + console.log(`Found ${tools.tools.length} tools:`); + for (const tool of tools.tools) { + const meta = tool._meta as Record | undefined; + const uiUri = meta?.["ui/resourceUri"]; + console.log( + ` - ${tool.name}${uiUri ? ` (UI: ${uiUri})` : ""}` + ); + } + console.log(); + + console.log("All tests passed!"); + await client.close(); + // Give the server a moment to clean up before we exit + setTimeout(() => process.exit(0), 100); +} + +main().catch((error) => { + console.error("Test failed:", error); + process.exit(1); +}); diff --git a/src/modules/mcp/services/mcp.ts b/src/modules/mcp/services/mcp.ts index b697f7d..2deaf75 100644 --- a/src/modules/mcp/services/mcp.ts +++ b/src/modules/mcp/services/mcp.ts @@ -1,6 +1,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import fs from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { CallToolRequestSchema, CompleteRequestSchema, @@ -293,7 +294,12 @@ export const createMcpServer = (): McpServerWrapper => { // MCP Apps UI resources if (uri === HELLO_WORLD_APP_URI) { - const distDir = path.join(import.meta.dirname, "../../../apps"); + // Always resolve to dist/apps from project root, regardless of whether + // we're running from src/ (tsx dev) or dist/ (production) + // Path: {src,dist}/modules/mcp/services/ -> 4 levels up to project root + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const projectRoot = path.join(__dirname, "../../../.."); + const distDir = path.join(projectRoot, "dist/apps"); const html = await fs.readFile(path.join(distDir, "mcp-app.html"), "utf-8"); return { contents: [ diff --git a/src/stdio.ts b/src/stdio.ts new file mode 100644 index 0000000..362fb75 --- /dev/null +++ b/src/stdio.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Stdio entrypoint for the MCP example server. + * This allows running the server locally via stdio transport. + * + * Usage: + * npx tsx src/stdio.ts + * # or after building: + * node dist/stdio.js + */ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createMcpServer } from "./modules/mcp/services/mcp.js"; + +async function main() { + const { server, cleanup } = createMcpServer(); + + const transport = new StdioServerTransport(); + + // Handle cleanup on exit + process.on("SIGINT", () => { + cleanup(); + process.exit(0); + }); + + process.on("SIGTERM", () => { + cleanup(); + process.exit(0); + }); + + await server.connect(transport); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +});