Skip to content

Commit 7bc1313

Browse files
Add stdio entrypoint for local development
Enables running the MCP server locally via stdio transport for testing with MCP clients without needing a full HTTP/Redis setup. Also fixes import.meta.dirname compatibility issue with tsx by using fileURLToPath(import.meta.url) instead. Usage: npm run dev:stdio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent e040655 commit 7bc1313

File tree

4 files changed

+121
-1
lines changed

4 files changed

+121
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"main": "dist/index.js",
77
"scripts": {
88
"dev": "tsx watch --inspect src/index.ts",
9+
"dev:stdio": "tsx src/stdio.ts",
910
"dev:internal": "AUTH_MODE=internal tsx watch --inspect src/index.ts",
1011
"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\"",
1112
"build": "tsc && npm run copy-static && npm run build:apps",

scripts/test-stdio.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Test script to verify the stdio server works correctly.
4+
* Connects to the server, lists resources, and reads the MCP App resource.
5+
*/
6+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8+
9+
async function main() {
10+
console.log("Starting stdio client test...\n");
11+
12+
const transport = new StdioClientTransport({
13+
command: "npx",
14+
args: ["tsx", "src/stdio.ts"],
15+
});
16+
17+
const client = new Client({
18+
name: "test-client",
19+
version: "1.0.0",
20+
});
21+
22+
await client.connect(transport);
23+
console.log("Connected to server\n");
24+
25+
// Initialize
26+
const serverInfo = client.getServerVersion();
27+
console.log("Server info:", serverInfo);
28+
console.log();
29+
30+
// List resources
31+
console.log("Listing resources...");
32+
const resources = await client.listResources();
33+
console.log(`Found ${resources.resources.length} resources:`);
34+
for (const resource of resources.resources) {
35+
console.log(` - ${resource.name} (${resource.uri})`);
36+
}
37+
console.log();
38+
39+
// Find and read the MCP App resource
40+
const mcpAppResource = resources.resources.find((r) =>
41+
r.uri.startsWith("ui://")
42+
);
43+
if (mcpAppResource) {
44+
console.log(`Reading MCP App resource: ${mcpAppResource.uri}`);
45+
const content = await client.readResource({ uri: mcpAppResource.uri });
46+
const textContent = content.contents[0];
47+
if ("text" in textContent) {
48+
console.log(` MIME type: ${textContent.mimeType}`);
49+
console.log(` Content length: ${textContent.text.length} bytes`);
50+
console.log(` First 200 chars: ${textContent.text.slice(0, 200)}...`);
51+
}
52+
console.log();
53+
}
54+
55+
// List tools
56+
console.log("Listing tools...");
57+
const tools = await client.listTools();
58+
console.log(`Found ${tools.tools.length} tools:`);
59+
for (const tool of tools.tools) {
60+
const meta = tool._meta as Record<string, unknown> | undefined;
61+
const uiUri = meta?.["ui/resourceUri"];
62+
console.log(
63+
` - ${tool.name}${uiUri ? ` (UI: ${uiUri})` : ""}`
64+
);
65+
}
66+
console.log();
67+
68+
console.log("All tests passed!");
69+
await client.close();
70+
// Give the server a moment to clean up before we exit
71+
setTimeout(() => process.exit(0), 100);
72+
}
73+
74+
main().catch((error) => {
75+
console.error("Test failed:", error);
76+
process.exit(1);
77+
});

src/modules/mcp/services/mcp.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
import fs from "node:fs/promises";
33
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
45
import {
56
CallToolRequestSchema,
67
CompleteRequestSchema,
@@ -293,7 +294,12 @@ export const createMcpServer = (): McpServerWrapper => {
293294

294295
// MCP Apps UI resources
295296
if (uri === HELLO_WORLD_APP_URI) {
296-
const distDir = path.join(import.meta.dirname, "../../../apps");
297+
// Always resolve to dist/apps from project root, regardless of whether
298+
// we're running from src/ (tsx dev) or dist/ (production)
299+
// Path: {src,dist}/modules/mcp/services/ -> 4 levels up to project root
300+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
301+
const projectRoot = path.join(__dirname, "../../../..");
302+
const distDir = path.join(projectRoot, "dist/apps");
297303
const html = await fs.readFile(path.join(distDir, "mcp-app.html"), "utf-8");
298304
return {
299305
contents: [

src/stdio.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Stdio entrypoint for the MCP example server.
4+
* This allows running the server locally via stdio transport.
5+
*
6+
* Usage:
7+
* npx tsx src/stdio.ts
8+
* # or after building:
9+
* node dist/stdio.js
10+
*/
11+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12+
import { createMcpServer } from "./modules/mcp/services/mcp.js";
13+
14+
async function main() {
15+
const { server, cleanup } = createMcpServer();
16+
17+
const transport = new StdioServerTransport();
18+
19+
// Handle cleanup on exit
20+
process.on("SIGINT", () => {
21+
cleanup();
22+
process.exit(0);
23+
});
24+
25+
process.on("SIGTERM", () => {
26+
cleanup();
27+
process.exit(0);
28+
});
29+
30+
await server.connect(transport);
31+
}
32+
33+
main().catch((error) => {
34+
console.error("Fatal error:", error);
35+
process.exit(1);
36+
});

0 commit comments

Comments
 (0)