Skip to content

Commit fabd0cf

Browse files
cablateclaude
andcommitted
feat: add stdio transport support + MCP Registry metadata
- Add --stdio flag for stdio transport mode (Claude Desktop, Cursor, etc.) - Add startStdio() method to BaseMcpServer - Add mcpName and server.json for MCP Registry publishing - Update README: stdio as Method 1 (recommended), HTTP as Method 2 - Add stdio transport tests (6 assertions, 80 total, 0 failures) - Update package.json description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent acdf7bb commit fabd0cf

6 files changed

Lines changed: 221 additions & 55 deletions

File tree

README.md

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -61,62 +61,48 @@ All tools are annotated with `readOnlyHint: true` and `destructiveHint: false`
6161
6262
## Installation
6363

64-
> **Note**: This server uses HTTP transport, not stdio. Direct npx usage in MCP Server Settings is **NOT supported**.
64+
### Method 1: stdio (Recommended for most clients)
6565

66-
### Method 1: Global Installation (Recommended)
66+
Works with Claude Desktop, Cursor, VS Code, and any MCP client that supports stdio:
6767

68-
```bash
69-
# Install globally
70-
npm install -g @cablate/mcp-google-map
71-
72-
# Run the server
73-
mcp-google-map --port 3000 --apikey "your_api_key_here"
74-
75-
# Using short options
76-
mcp-google-map -p 3000 -k "your_api_key_here"
68+
```json
69+
{
70+
"mcpServers": {
71+
"google-maps": {
72+
"command": "npx",
73+
"args": ["-y", "@cablate/mcp-google-map", "--stdio"],
74+
"env": {
75+
"GOOGLE_MAPS_API_KEY": "YOUR_API_KEY"
76+
}
77+
}
78+
}
79+
}
7780
```
7881

79-
### Method 2: Using npx (Quick Start)
80-
81-
> Cannot be used directly in MCP Server Settings with stdio mode
82+
### Method 2: HTTP Server
8283

83-
**Step 1: Launch HTTP Server in Terminal**
84+
For multi-session deployments, per-request API key isolation, or remote access:
8485

8586
```bash
86-
# Run in a separate terminal
8787
npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
88-
89-
# Or with environment variable
90-
GOOGLE_MAPS_API_KEY=YOUR_API_KEY npx @cablate/mcp-google-map
91-
```
92-
93-
**Step 2: Configure MCP Client to Use HTTP**
94-
95-
```json
96-
{
97-
"mcp-google-map": {
98-
"transport": "http",
99-
"url": "http://localhost:3000/mcp"
100-
}
101-
}
10288
```
10389

104-
### Common Mistake to Avoid
90+
Then configure your MCP client:
10591

10692
```json
107-
// This WILL NOT WORK - stdio mode not supported with npx
10893
{
109-
"mcp-google-map": {
110-
"command": "npx",
111-
"args": ["@cablate/mcp-google-map"]
94+
"mcpServers": {
95+
"google-maps": {
96+
"type": "http",
97+
"url": "http://localhost:3000/mcp"
98+
}
11299
}
113100
}
114101
```
115102

116103
### Server Information
117104

118-
- **Endpoint**: `http://localhost:3000/mcp`
119-
- **Transport**: Streamable HTTP (not stdio)
105+
- **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
120106
- **Tools**: 8 Google Maps tools
121107

122108
### CLI Exec Mode (Agent Skill)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "@cablate/mcp-google-map",
33
"version": "0.0.25",
4-
"description": "Google Maps MCP server with streamable HTTP transport support for location services, geocoding, and navigation",
4+
"mcpName": "io.github.cablate/google-map",
5+
"description": "Google Maps tools for AI agents — 8 tools (geocode, search, directions, elevation) via MCP server or standalone Agent Skill CLI",
56
"type": "module",
67
"main": "dist/index.js",
78
"bin": {

server.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.cablate/google-map",
4+
"title": "Google Maps MCP Server",
5+
"description": "Google Maps tools for AI agents — 8 tools (geocode, search, directions, elevation) via MCP server or standalone Agent Skill CLI. The only maps MCP with Agent Skill definitions, exec CLI mode, and StreamableHTTP multi-session support.",
6+
"repository": {
7+
"url": "https://github.com/cablate/mcp-google-map",
8+
"source": "github"
9+
},
10+
"version": "0.0.24",
11+
"packages": [
12+
{
13+
"registryType": "npm",
14+
"identifier": "@cablate/mcp-google-map",
15+
"version": "0.0.24",
16+
"transport": {
17+
"type": "stdio"
18+
},
19+
"environmentVariables": [
20+
{
21+
"name": "GOOGLE_MAPS_API_KEY",
22+
"description": "Google Maps API key (get one at https://console.cloud.google.com)",
23+
"isRequired": true,
24+
"format": "string",
25+
"isSecret": true
26+
}
27+
]
28+
}
29+
]
30+
}

src/cli.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ if (isRunDirectly || isMainModule) {
216216
)
217217
.command(
218218
"$0",
219-
"Start the MCP server",
219+
"Start the MCP server (HTTP by default, --stdio for stdio mode)",
220220
(yargs) => {
221221
return yargs
222222
.option("port", {
@@ -231,26 +231,43 @@ if (isRunDirectly || isMainModule) {
231231
description: "Google Maps API key",
232232
default: process.env.GOOGLE_MAPS_API_KEY,
233233
})
234+
.option("stdio", {
235+
type: "boolean",
236+
description: "Use stdio transport instead of HTTP",
237+
default: false,
238+
})
234239
.example([
235-
["$0", "Start server with default settings"],
236-
['$0 --port 3000 --apikey "your_api_key"', "Start with custom port and API key"],
240+
["$0", "Start HTTP server with default settings"],
241+
['$0 --port 3000 --apikey "your_api_key"', "Start HTTP with custom port and API key"],
242+
["$0 --stdio", "Start in stdio mode (for Claude Desktop, Cursor, etc.)"],
237243
]);
238244
},
239245
async (argv) => {
240-
Logger.log("🗺️ Google Maps MCP Server");
241-
Logger.log(" A Model Context Protocol server for Google Maps services");
242-
Logger.log("");
246+
if (argv.apikey) {
247+
process.env.GOOGLE_MAPS_API_KEY = argv.apikey as string;
248+
}
243249

244-
if (!argv.apikey) {
245-
Logger.log("⚠️ Google Maps API Key not found!");
246-
Logger.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file");
250+
if (argv.stdio) {
251+
// stdio mode — all logs go to stderr, stdout reserved for JSON-RPC
252+
const server = new BaseMcpServer(serverConfigs[0].name, serverConfigs[0].tools);
253+
await server.startStdio();
254+
} else {
255+
// HTTP mode
256+
Logger.log("🗺️ Google Maps MCP Server");
257+
Logger.log(" A Model Context Protocol server for Google Maps services");
247258
Logger.log("");
248-
}
249259

250-
startServer(argv.port as number, argv.apikey as string).catch((error) => {
251-
Logger.error("❌ Failed to start server:", error);
252-
process.exit(1);
253-
});
260+
if (!argv.apikey) {
261+
Logger.log("⚠️ Google Maps API Key not found!");
262+
Logger.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file");
263+
Logger.log("");
264+
}
265+
266+
startServer(argv.port as number, argv.apikey as string).catch((error) => {
267+
Logger.error("❌ Failed to start server:", error);
268+
process.exit(1);
269+
});
270+
}
254271
}
255272
)
256273
.version(packageVersion)

src/core/BaseMcpServer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
34
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
45
import { isInitializeRequest, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
@@ -179,6 +180,11 @@ export class BaseMcpServer {
179180
});
180181
}
181182

183+
async startStdio(): Promise<void> {
184+
const transport = new StdioServerTransport();
185+
await this.connect(transport);
186+
}
187+
182188
async stopHttpServer(): Promise<void> {
183189
if (!this.httpServer) {
184190
// Changed to Logger.warn and return, as throwing an error might be too harsh if called multiple times.

tests/smoke.test.ts

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -454,10 +454,135 @@ async function testMultiSession(): Promise<void> {
454454
}
455455
}
456456

457-
// --------------- Test 6: CLI Exec Mode ---------------
457+
// --------------- Test 6: Stdio Transport ---------------
458+
459+
async function testStdio(): Promise<void> {
460+
console.log("\n🧪 Test 6: Stdio transport");
461+
462+
const { spawn } = await import("node:child_process");
463+
const { resolve } = await import("node:path");
464+
const cliPath = resolve(import.meta.dirname ?? ".", "../dist/cli.js");
465+
466+
// Helper: send a JSON-RPC message over stdio and collect the response
467+
const stdioCall = (messages: object[]): Promise<string[]> => {
468+
return new Promise((resolvePromise, reject) => {
469+
const args = ["--stdio"];
470+
if (API_KEY) args.push("--apikey", API_KEY);
471+
472+
const child = spawn("node", [cliPath, ...args], {
473+
stdio: ["pipe", "pipe", "pipe"],
474+
});
475+
476+
let stdout = "";
477+
child.stdout!.on("data", (chunk: Buffer) => {
478+
stdout += chunk.toString();
479+
});
480+
481+
const timeout = setTimeout(() => {
482+
child.kill();
483+
reject(new Error("stdio test timed out"));
484+
}, 15000);
485+
486+
child.on("close", () => {
487+
clearTimeout(timeout);
488+
resolvePromise(stdout.split("\n").filter((l) => l.trim()));
489+
});
490+
491+
// Send all messages then close stdin
492+
for (const msg of messages) {
493+
child.stdin!.write(JSON.stringify(msg) + "\n");
494+
}
495+
child.stdin!.end();
496+
});
497+
};
498+
499+
// Test: initialize
500+
try {
501+
const lines = await stdioCall([
502+
{
503+
jsonrpc: "2.0",
504+
id: 1,
505+
method: "initialize",
506+
params: {
507+
protocolVersion: PROTOCOL_VERSION,
508+
capabilities: {},
509+
clientInfo: { name: "stdio-test", version: "1.0.0" },
510+
},
511+
},
512+
]);
513+
assert(lines.length > 0, "stdio: initialize returns response");
514+
const resp = JSON.parse(lines[0]);
515+
assert(resp?.result?.serverInfo?.name !== undefined, "stdio: server info present");
516+
assert(resp?.result?.capabilities?.tools !== undefined, "stdio: tools capability present");
517+
} catch (err: any) {
518+
assert(false, "stdio: initialize succeeds", err.message);
519+
}
520+
521+
// Test: initialize + list tools
522+
try {
523+
const lines = await stdioCall([
524+
{
525+
jsonrpc: "2.0",
526+
id: 1,
527+
method: "initialize",
528+
params: {
529+
protocolVersion: PROTOCOL_VERSION,
530+
capabilities: {},
531+
clientInfo: { name: "stdio-test", version: "1.0.0" },
532+
},
533+
},
534+
{ jsonrpc: "2.0", method: "notifications/initialized" },
535+
{ jsonrpc: "2.0", id: 2, method: "tools/list" },
536+
]);
537+
// Find tools/list response
538+
const toolsResp = lines.map((l) => JSON.parse(l)).find((m: any) => m.id === 2);
539+
const tools = toolsResp?.result?.tools ?? [];
540+
assert(tools.length >= 8, `stdio: tools/list returns ${tools.length} tools`);
541+
} catch (err: any) {
542+
assert(false, "stdio: tools/list succeeds", err.message);
543+
}
544+
545+
// Test: tool call (geocode) via stdio
546+
if (API_KEY) {
547+
try {
548+
const lines = await stdioCall([
549+
{
550+
jsonrpc: "2.0",
551+
id: 1,
552+
method: "initialize",
553+
params: {
554+
protocolVersion: PROTOCOL_VERSION,
555+
capabilities: {},
556+
clientInfo: { name: "stdio-test", version: "1.0.0" },
557+
},
558+
},
559+
{ jsonrpc: "2.0", method: "notifications/initialized" },
560+
{
561+
jsonrpc: "2.0",
562+
id: 2,
563+
method: "tools/call",
564+
params: { name: "maps_geocode", arguments: { address: "Tokyo Tower" } },
565+
},
566+
]);
567+
const geocodeResp = lines.map((l) => JSON.parse(l)).find((m: any) => m.id === 2);
568+
const content = geocodeResp?.result?.content ?? [];
569+
assert(content.length > 0, "stdio: geocode returns content");
570+
if (content.length > 0) {
571+
const parsed = JSON.parse(content[0].text);
572+
assert(typeof parsed?.location?.lat === "number", "stdio: geocode returns lat");
573+
}
574+
} catch (err: any) {
575+
assert(false, "stdio: geocode succeeds", err.message);
576+
}
577+
} else {
578+
console.log(" ⏭️ stdio tool call skipped (no GOOGLE_MAPS_API_KEY)");
579+
}
580+
}
581+
582+
// --------------- Test 7: CLI Exec Mode ---------------
458583

459584
async function testExecMode(): Promise<void> {
460-
console.log("\n🧪 Test 6: CLI exec mode");
585+
console.log("\n🧪 Test 7: CLI exec mode");
461586

462587
const { execFileSync } = await import("node:child_process");
463588
const { resolve } = await import("node:path");
@@ -545,7 +670,8 @@ async function main() {
545670
console.log(` API Key: ${API_KEY ? "✅ provided" : "⚠️ not set (some tests skipped)"}`);
546671
console.log("═══════════════════════════════════════════");
547672

548-
// Test exec mode first (no server needed)
673+
// Test stdio and exec mode first (no server needed)
674+
await testStdio();
549675
await testExecMode();
550676

551677
try {

0 commit comments

Comments
 (0)