@ikeytz/maps-mcp 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -1,52 +1,166 @@
1
- import { spawn } from "node:child_process";
2
- import { callTool, mapsMcpUrl, mapsRpc, packageVersion, showMcpCommand, toolByName, toolNames } from "./proxy.js";
1
+ /**
2
+ * Human terminal CLI over the same remote MCP SSOT (maps.ikeytz.com).
3
+ * Subcommands: help | list | describe | call | <toolName>
4
+ * No args → stdio proxy (MCP hosts).
5
+ * Structure aligned with @ikeytz/mcp (ikeytz-mcp) — other remote, maps-only tools.
6
+ */
3
7
 
4
- const TOOL_FLAGS = [
5
- "slug",
6
- "plz",
7
- "name",
8
- "which",
9
- "offset",
10
- "limit",
11
- "q",
12
- "path",
13
- "url",
14
- "icbm",
15
- "lat",
16
- "lng",
17
- "radius_km",
18
- "doc",
19
- ];
20
- const NUMBER_FLAGS = new Set(["offset", "limit", "lat", "lng", "radius_km"]);
8
+ import { spawnSync } from "node:child_process";
9
+ import { stderr as log, stdout as out } from "node:process";
10
+ import {
11
+ CATALOG,
12
+ callTool,
13
+ mapsMcpProtocol,
14
+ mapsMcpUrl,
15
+ mapsRpc,
16
+ packageVersion,
17
+ toolByName,
18
+ } from "./proxy.js";
19
+
20
+ function catalogTools() {
21
+ return CATALOG.tools || [];
22
+ }
23
+
24
+ function exampleForTool(name) {
25
+ const p = "ikeytz-maps-mcp";
26
+ const map = {
27
+ list_service_areas: `${p} list_service_areas`,
28
+ get_service_area: `${p} get_service_area --slug=pattonville`,
29
+ get_geo_office: `${p} get_geo_office`,
30
+ get_contact: `${p} get_contact`,
31
+ get_discovery: `${p} get_discovery --which=llms-urheberrecht`,
32
+ get_llms_mcp_server: `${p} get_llms_mcp_server`,
33
+ get_llms_mcp_web: `${p} get_llms_mcp_web`,
34
+ find_by_keyword: `${p} find_by_keyword --q=Pattonville --limit=5`,
35
+ find_by_serp: `${p} find_by_serp --q=Grünbühl`,
36
+ get_serp_snippet: `${p} get_serp_snippet --slug=pattonville`,
37
+ find_by_plz: `${p} find_by_plz --plz=71638`,
38
+ find_by_geo: `${p} find_by_geo --lat=48.873174 --lng=9.224754`,
39
+ find_by_icbm: `${p} find_by_icbm --icbm=48.873174,9.224754`,
40
+ find_by_gebiet: `${p} find_by_gebiet --q=Grünbühl`,
41
+ find_by_area: `${p} find_by_area --q=Grünbühl`,
42
+ list_slugs: `${p} list_slugs`,
43
+ list_plz: `${p} list_plz`,
44
+ get_hub: `${p} get_hub`,
45
+ get_embed_map: `${p} get_embed_map --slug=pattonville`,
46
+ get_geo_api: `${p} get_geo_api --slug=pattonville`,
47
+ get_lage: `${p} get_lage --slug=pattonville`,
48
+ get_geschichte: `${p} get_geschichte --slug=pattonville`,
49
+ distance_to_office: `${p} distance_to_office --slug=pattonville`,
50
+ resolve_maps_url: `${p} resolve_maps_url --url=https://maps.ikeytz.com/pattonville`,
51
+ find_by_google_maps: `${p} find_by_google_maps --q=48.873174,9.224754`,
52
+ get_legal: `${p} get_legal --doc=urheberrecht`,
53
+ get_urheberrecht: `${p} get_urheberrecht`,
54
+ get_copyright: `${p} get_copyright`,
55
+ get_impressum: `${p} get_impressum`,
56
+ get_agb: `${p} get_agb`,
57
+ get_nutzungsbedingungen: `${p} get_nutzungsbedingungen`,
58
+ get_datenschutz: `${p} get_datenschutz`,
59
+ get_widerruf: `${p} get_widerruf`,
60
+ site_overview: `${p} site_overview --locale=de`,
61
+ get_business_identity: `${p} get_business_identity --locale=de`,
62
+ list_nav: `${p} list_nav --locale=de`,
63
+ list_footer: `${p} list_footer --locale=de`,
64
+ list_locales: `${p} list_locales`,
65
+ resolve_locale_url: `${p} resolve_locale_url --path=/pattonville --locale=de`,
66
+ get_prices: `${p} get_prices --locale=de`,
67
+ get_about: `${p} get_about --locale=de`,
68
+ get_links: `${p} get_links --locale=de`,
69
+ get_llms_txt: `${p} get_llms_txt`,
70
+ get_sitemap_txt: `${p} get_sitemap_txt`,
71
+ compose_tel_mobile: `${p} compose_tel_mobile`,
72
+ compose_mailto: `${p} compose_mailto`,
73
+ };
74
+ return map[name] || `${p} ${name}`;
75
+ }
76
+
77
+ function argHint(tool) {
78
+ const props = Object.keys(tool.inputSchema?.properties || {});
79
+ const req = tool.inputSchema?.required || [];
80
+ if (!props.length) return "";
81
+ return props.map((k) => (req.includes(k) ? `--${k}*` : `--${k}`)).join(" ");
82
+ }
83
+
84
+ function formatToolsAsciiTable(rows) {
85
+ const n = rows.length;
86
+ const cn = String(n).length;
87
+ const c0 = Math.max(4, ...rows.map((r) => r.tool.length), "TOOL".length);
88
+ const c1 = Math.max(4, ...rows.map((r) => r.args.length), "ARGS".length);
89
+ const pad = (s, w, align = "left") => {
90
+ const t = String(s);
91
+ if (t.length >= w) return t;
92
+ const sp = " ".repeat(w - t.length);
93
+ return align === "right" ? sp + t : t + sp;
94
+ };
95
+ const rule = `${"─".repeat(cn)}─┼─${"─".repeat(c0)}─┼─${"─".repeat(c1)}─┼─${"─".repeat(7)}`;
96
+ const lines = [`${pad("#", cn, "right")} │ ${pad("TOOL", c0)} │ ${pad("ARGS", c1)} │ EXAMPLE`, rule];
97
+ rows.forEach((r, i) => {
98
+ lines.push(
99
+ `${pad(String(i + 1), cn, "right")} │ ${pad(r.tool, c0)} │ ${pad(r.args, c1)} │ ${r.example}`
100
+ );
101
+ lines.push(rule);
102
+ });
103
+ return lines;
104
+ }
21
105
 
22
106
  function globalCliFlagsLines() {
23
107
  return [
108
+ "",
24
109
  "Global CLI flags (every tool — not sent to MCP)",
25
110
  " -h, --help, -help per-tool help (offline)",
26
111
  " --raw JSON structuredContent",
27
- " --show-mcp-command curl 1a/1b/2 only — no MCP call (alias: --show-mcp-befehl)",
28
- " --json '{…}' raw MCP arguments",
112
+ " --show-mcp-command curl 1a/1b/2 only — no MCP call",
113
+ " aliases: --show-mcp-befehl · --show-mcp-comment",
29
114
  "",
30
115
  "Default tool output payload only (no ok/tool/meta envelope)",
31
- " --name=value --name value",
116
+ " --name=value --name value --json '{...}'",
32
117
  ];
33
118
  }
34
119
 
35
- function exampleForTool(name) {
36
- const examples = {
37
- get_service_area: "ikeytz-maps-mcp get_service_area --slug=pattonville",
38
- get_discovery: "ikeytz-maps-mcp get_discovery --which=llms-urheberrecht",
39
- find_by_plz: "ikeytz-maps-mcp find_by_plz --plz=71638",
40
- get_legal: "ikeytz-maps-mcp get_legal --doc=urheberrecht",
41
- get_urheberrecht: "ikeytz-maps-mcp get_urheberrecht",
42
- get_agb: "ikeytz-maps-mcp get_agb",
43
- get_nutzungsbedingungen: "ikeytz-maps-mcp get_nutzungsbedingungen",
44
- get_lage: "ikeytz-maps-mcp get_lage --slug=pattonville",
45
- };
46
- return examples[name] || `ikeytz-maps-mcp ${name}`;
120
+ function printHumanHelp(tools) {
121
+ const lines = [
122
+ `ikeytz-maps-mcp ${packageVersion()}`,
123
+ `Remote ${mapsMcpUrl()}`,
124
+ `Registry com.ikeytz/maps`,
125
+ "Nur maps. Nicht @ikeytz/mcp / www.",
126
+ "",
127
+ "Usage",
128
+ " ikeytz-maps-mcp MCP proxy (Cursor/Claude)",
129
+ " ikeytz-maps-mcp --version package version (offline)",
130
+ " ikeytz-maps-mcp help this help (offline, no MCP connect)",
131
+ " ikeytz-maps-mcp update [version] npm i -g @ikeytz/maps-mcp@latest",
132
+ " ikeytz-maps-mcp list tool table (offline) + global flags",
133
+ " ikeytz-maps-mcp list --global global CLI flags only (offline)",
134
+ " ikeytz-maps-mcp describe <tool> args + examples (offline)",
135
+ " ikeytz-maps-mcp call <tool> [flags]",
136
+ " ikeytz-maps-mcp <tool> [flags] same as call",
137
+ " ikeytz-maps-mcp <tool> -h per-tool help (offline, same as describe)",
138
+ " ikeytz-maps-mcp <tool> --show-mcp-command",
139
+ "",
140
+ ...globalCliFlagsLines().slice(1),
141
+ "",
142
+ ];
143
+ if (tools?.length) {
144
+ const rows = [...tools]
145
+ .sort((a, b) => a.name.localeCompare(b.name))
146
+ .map((t) => ({
147
+ tool: t.name,
148
+ args: argHint(t) || "—",
149
+ example: exampleForTool(t.name),
150
+ }));
151
+ lines.push(`Tools ${rows.length} (A–Z numbered, * = required)`);
152
+ lines.push("");
153
+ lines.push(...formatToolsAsciiTable(rows));
154
+ lines.push("");
155
+ lines.push("Tip ikeytz-maps-mcp describe get_service_area");
156
+ } else {
157
+ lines.push("Tools ikeytz-maps-mcp list (offline) · ikeytz-maps-mcp <tool> -h for args");
158
+ }
159
+ lines.push("");
160
+ out.write(`${lines.join("\n")}\n`);
47
161
  }
48
162
 
49
- function formatToolHelp(tool) {
163
+ function formatDescribe(tool) {
50
164
  const schema = tool.inputSchema || { type: "object", properties: {} };
51
165
  const props = schema.properties || {};
52
166
  const req = new Set(schema.required || []);
@@ -59,7 +173,8 @@ function formatToolHelp(tool) {
59
173
  const p = props[key] || {};
60
174
  const type = p.enum ? p.enum.join("|") : p.type || "any";
61
175
  const star = req.has(key) ? "*" : " ";
62
- lines.push(` ${star} --${key.padEnd(16)} ${type}`);
176
+ const desc = p.description ? ` ${p.description}` : "";
177
+ lines.push(` ${star} --${key.padEnd(16)} ${type}${desc}`);
63
178
  }
64
179
  if (req.size) {
65
180
  lines.push("");
@@ -70,119 +185,156 @@ function formatToolHelp(tool) {
70
185
  lines.push("CLI flags (not sent to MCP)");
71
186
  lines.push(" -h, --help, -help this help (offline, no MCP call)");
72
187
  lines.push(" --raw JSON structuredContent");
73
- lines.push(" --show-mcp-command curl 1a/1b/2 only — no MCP call (alias: --show-mcp-befehl)");
188
+ lines.push(" --show-mcp-command curl 1a/1b/2 only (aliases: --show-mcp-befehl, --show-mcp-comment)");
74
189
  lines.push("");
75
190
  lines.push("Examples");
76
191
  lines.push(` ${exampleForTool(tool.name)}`);
77
192
  lines.push(` ikeytz-maps-mcp ${tool.name} -h`);
78
193
  lines.push(` ikeytz-maps-mcp ${tool.name} --show-mcp-command`);
79
- return `${lines.join("\n")}\n`;
194
+ return lines.filter((l, i) => !(i === 1 && !l)).join("\n");
80
195
  }
81
196
 
82
- const HELP = `ikeytz-maps-mcp ${packageVersion()}
83
- Remote: ${mapsMcpUrl()}
84
- Nur maps. Nicht @ikeytz/mcp / www.
197
+ const SHOW_MCP_COMMAND_FLAGS = new Set([
198
+ "--show-mcp-command",
199
+ "--show-mcp-befehl",
200
+ "--show-mcp-comment",
201
+ ]);
85
202
 
86
- Ohne Args stdio-Proxy (Cursor/Claude)
87
- help | --help diese Hilfe
88
- --version Version
89
- update [ver] npm i -g @ikeytz/maps-mcp@latest
90
- list ${toolNames().length} Tools (offline) + global CLI flags
91
- list --global nur globale CLI-Flags (offline)
92
- describe <tool> Args + Flags + Beispiele (offline)
93
- call <tool> … wie <tool> …
94
- <tool> Payload von maps /mcp
95
- <tool> -h Args + CLI-Flags (offline)
96
- <tool> --show-mcp-command | --show-mcp-befehl
97
- <tool> --raw JSON structuredContent
98
- <tool> --json '{…}'
203
+ function wantsShowMcpCommand(argv) {
204
+ return argv.some((a) => SHOW_MCP_COMMAND_FLAGS.has(a));
205
+ }
99
206
 
100
- ${globalCliFlagsLines().join("\n")}
207
+ function stripCliDisplayFlags(argv) {
208
+ return argv.filter((a) => !SHOW_MCP_COMMAND_FLAGS.has(a));
209
+ }
101
210
 
102
- Tools:
103
- ${toolNames()
104
- .map((n) => ` ${n}`)
105
- .join("\n")}
106
- `;
211
+ function isHelpFlag(a) {
212
+ return a === "-h" || a === "--help" || a === "-help" || a === "help";
213
+ }
107
214
 
108
- function parseArgv(argv) {
109
- const flags = {};
110
- const positionals = [];
111
- for (let i = 0; i < argv.length; i++) {
215
+ function parseToolArgs(argv) {
216
+ const args = {};
217
+ let i = 0;
218
+ while (i < argv.length) {
112
219
  const a = argv[i];
113
- if (a === "--raw") flags.raw = true;
114
- else if (a === "--global") flags.global = true;
115
- else if (a === "-h" || a === "--help" || a === "-help") flags.help = true;
116
- else if (a === "--version") flags.version = true;
117
- else if (a === "--show-mcp-command" || a === "--show-mcp-befehl") flags.showMcp = true;
118
- else if (a === "--json") flags.json = argv[++i] || "";
119
- else if (a.startsWith("--json=")) flags.json = a.slice(7);
120
- else if (a.startsWith("--")) {
220
+ if (a === "--json") {
221
+ const raw = argv[i + 1];
222
+ if (!raw) throw new Error("--json needs a JSON object");
223
+ const parsed = JSON.parse(raw);
224
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
225
+ throw new Error("--json must be an object");
226
+ }
227
+ Object.assign(args, parsed);
228
+ i += 2;
229
+ continue;
230
+ }
231
+ if (a.startsWith("--json=")) {
232
+ Object.assign(args, JSON.parse(a.slice("--json=".length)));
233
+ i += 1;
234
+ continue;
235
+ }
236
+ if (a.startsWith("--")) {
121
237
  const eq = a.indexOf("=");
122
- if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
123
- else {
124
- const key = a.slice(2);
238
+ let key;
239
+ let val;
240
+ if (eq > 2) {
241
+ key = a.slice(2, eq);
242
+ val = a.slice(eq + 1);
243
+ i += 1;
244
+ } else {
245
+ key = a.slice(2);
125
246
  const next = argv[i + 1];
126
- if (next && !next.startsWith("-")) {
127
- flags[key] = next;
247
+ if (!next || next.startsWith("-")) {
248
+ val = true;
128
249
  i += 1;
129
- } else flags[key] = true;
250
+ } else {
251
+ val = next;
252
+ i += 2;
253
+ }
130
254
  }
131
- } else positionals.push(a);
255
+ if (val === "true") val = true;
256
+ else if (val === "false") val = false;
257
+ else if (typeof val === "string" && /^-?\d+(\.\d+)?$/.test(val)) val = Number(val);
258
+ args[key] = val;
259
+ continue;
260
+ }
261
+ throw new Error(`Unexpected argument: ${a}`);
132
262
  }
133
- return { flags, positionals };
263
+ return args;
134
264
  }
135
265
 
136
- function argsFromFlags(flags) {
137
- const args = {};
138
- if (flags.json) Object.assign(args, JSON.parse(flags.json));
139
- for (const key of TOOL_FLAGS) {
140
- if (flags[key] != null && flags[key] !== true) {
141
- args[key] = NUMBER_FLAGS.has(key) ? Number(flags[key]) : String(flags[key]);
142
- }
266
+ function formatMcpCurlHeading(toolName, callArgs = {}) {
267
+ const url = mapsMcpUrl();
268
+ const proto = mapsMcpProtocol();
269
+ const argsObj = callArgs && typeof callArgs === "object" ? callArgs : {};
270
+ const argsJson = JSON.stringify(argsObj);
271
+ const callBody = JSON.stringify({
272
+ jsonrpc: "2.0",
273
+ id: 2,
274
+ method: "tools/call",
275
+ params: { name: toolName, arguments: argsObj },
276
+ });
277
+ const dq = (s) => String(s).replace(/'/g, `'\\''`);
278
+ const tool = toolByName(toolName);
279
+ const blurb = tool?.description || `Remote MCP tools/call · ${toolName}`;
280
+ const parts = [`ikeytz-maps-mcp ${toolName}`];
281
+ for (const [k, v] of Object.entries(argsObj)) {
282
+ if (v === undefined) continue;
283
+ parts.push(`--${k}=${v}`);
143
284
  }
144
- return args;
285
+ const bar = "═".repeat(56);
286
+ const thin = "─".repeat(56);
287
+ return [
288
+ bar,
289
+ `MCP tool ${toolName}`,
290
+ `What ${blurb}`,
291
+ `CLI ${parts.join(" ")}`,
292
+ `Args ${argsJson}`,
293
+ `Registry com.ikeytz/maps`,
294
+ `Remote ${url}`,
295
+ thin,
296
+ `Equivalent curl (same MCP call)`,
297
+ "",
298
+ `# 1a) Session-ID holen`,
299
+ `SID=$(curl -sS -D - -o /tmp/ikeytz-maps-mcp-init.json -X POST '${url}' \\`,
300
+ ` -H 'Content-Type: application/json' \\`,
301
+ ` -H 'Accept: application/json' \\`,
302
+ ` -H 'MCP-Protocol-Version: ${proto}' \\`,
303
+ ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"${proto}","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \\`,
304
+ ` | awk -F': ' 'tolower($1)=="mcp-session-id"{gsub(/\\r/,"",$2); print $2; exit}')`,
305
+ `echo "SID=$SID"`,
306
+ "",
307
+ `# 1b) initialized`,
308
+ `curl -sS -X POST '${url}' \\`,
309
+ ` -H 'Content-Type: application/json' \\`,
310
+ ` -H 'Accept: application/json' \\`,
311
+ ` -H 'MCP-Protocol-Version: ${proto}' \\`,
312
+ ` -H "Mcp-Session-Id: $SID" \\`,
313
+ ` -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null`,
314
+ "",
315
+ `# 2) Tool`,
316
+ `curl -sS -X POST '${url}' \\`,
317
+ ` -H 'Content-Type: application/json' \\`,
318
+ ` -H 'Accept: application/json' \\`,
319
+ ` -H 'MCP-Protocol-Version: ${proto}' \\`,
320
+ ` -H "Mcp-Session-Id: $SID" \\`,
321
+ ` -d '${dq(callBody)}'`,
322
+ bar,
323
+ ].join("\n");
145
324
  }
146
325
 
147
- function printPayload(rpc, raw) {
326
+ function printCallResult(rpc, { raw = false } = {}) {
148
327
  if (raw) {
149
- process.stdout.write(`${JSON.stringify(rpc, null, 2)}\n`);
328
+ out.write(`${JSON.stringify(rpc, null, 2)}\n`);
150
329
  return;
151
330
  }
152
331
  const data = rpc?.result?.structuredContent ?? rpc?.result ?? rpc;
153
- process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
154
- }
155
-
156
- function readStdioMessage(buf) {
157
- const text = buf.toString("utf8");
158
- const headerEnd = text.indexOf("\r\n\r\n");
159
- const altEnd = text.indexOf("\n\n");
160
- if (headerEnd >= 0 || (altEnd >= 0 && /content-length:/i.test(text.slice(0, altEnd + 2)))) {
161
- const split = headerEnd >= 0 ? headerEnd : altEnd;
162
- const sep = headerEnd >= 0 ? 4 : 2;
163
- const header = text.slice(0, split);
164
- const m = header.match(/content-length:\s*(\d+)/i);
165
- if (!m) return null;
166
- const len = Number(m[1]);
167
- const start = split + sep;
168
- const body = Buffer.from(text, "utf8").subarray(start);
169
- if (body.length < len) return null;
170
- const json = JSON.parse(body.subarray(0, len).toString("utf8"));
171
- return { json, rest: body.subarray(len) };
332
+ if (data && typeof data === "object") {
333
+ const { ok, tool, locale, related, attribution, ...payload } = data;
334
+ out.write(`${JSON.stringify(Object.keys(payload).length ? payload : data, null, 2)}\n`);
335
+ return;
172
336
  }
173
- const nl = text.indexOf("\n");
174
- if (nl < 0) return null;
175
- const line = text.slice(0, nl).trim();
176
- const rest = Buffer.from(text.slice(nl + 1), "utf8");
177
- if (!line) return { json: null, rest };
178
- return { json: JSON.parse(line), rest };
179
- }
180
-
181
- function writeStdio(msg) {
182
- const json = JSON.stringify(msg);
183
- const body = Buffer.from(json, "utf8");
184
- process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
185
- process.stdout.write(body);
337
+ out.write(`${JSON.stringify(data, null, 2)}\n`);
186
338
  }
187
339
 
188
340
  async function runStdioProxy() {
@@ -208,22 +360,22 @@ async function runStdioProxy() {
208
360
  const id = msg.id;
209
361
  try {
210
362
  if (method === "initialize") {
211
- const out = await mapsRpc("initialize", msg.params || {}, { id: id ?? 1 });
212
- sessionId = out.sessionId || sessionId;
213
- writeStdio(out.json);
363
+ const res = await mapsRpc("initialize", msg.params || {}, { id: id ?? 1 });
364
+ sessionId = res.sessionId || sessionId;
365
+ writeStdio(res.json);
214
366
  } else if (method === "notifications/initialized" || method === "initialized") {
215
- const out = await mapsRpc("notifications/initialized", msg.params || {}, {
367
+ const res = await mapsRpc("notifications/initialized", msg.params || {}, {
216
368
  notification: true,
217
369
  sessionId,
218
370
  });
219
- sessionId = out.sessionId || sessionId;
371
+ sessionId = res.sessionId || sessionId;
220
372
  if (id !== undefined) writeStdio({ jsonrpc: "2.0", id, result: {} });
221
373
  } else if (id === undefined) {
222
374
  await mapsRpc(method, msg.params || {}, { notification: true, sessionId });
223
375
  } else {
224
- const out = await mapsRpc(method, msg.params || {}, { id, sessionId });
225
- sessionId = out.sessionId || sessionId;
226
- writeStdio(out.json);
376
+ const res = await mapsRpc(method, msg.params || {}, { id, sessionId });
377
+ sessionId = res.sessionId || sessionId;
378
+ writeStdio(res.json);
227
379
  }
228
380
  } catch (err) {
229
381
  if (id !== undefined) {
@@ -235,72 +387,131 @@ async function runStdioProxy() {
235
387
  });
236
388
  }
237
389
 
238
- export async function main(argv) {
239
- const { flags, positionals } = parseArgv(argv);
240
-
241
- if (flags.version && positionals.length === 0) {
242
- process.stdout.write(`${packageVersion()}\n`);
243
- return;
390
+ function readStdioMessage(buf) {
391
+ const text = buf.toString("utf8");
392
+ const headerEnd = text.indexOf("\r\n\r\n");
393
+ const altEnd = text.indexOf("\n\n");
394
+ if (headerEnd >= 0 || (altEnd >= 0 && /content-length:/i.test(text.slice(0, altEnd + 2)))) {
395
+ const split = headerEnd >= 0 ? headerEnd : altEnd;
396
+ const sep = headerEnd >= 0 ? 4 : 2;
397
+ const header = text.slice(0, split);
398
+ const m = header.match(/content-length:\s*(\d+)/i);
399
+ if (!m) return null;
400
+ const len = Number(m[1]);
401
+ const start = split + sep;
402
+ const body = Buffer.from(text, "utf8").subarray(start);
403
+ if (body.length < len) return null;
404
+ return { json: JSON.parse(body.subarray(0, len).toString("utf8")), rest: body.subarray(len) };
244
405
  }
245
- if (argv.length === 0) {
406
+ const nl = text.indexOf("\n");
407
+ if (nl < 0) return null;
408
+ const line = text.slice(0, nl).trim();
409
+ const rest = Buffer.from(text.slice(nl + 1), "utf8");
410
+ if (!line) return { json: null, rest };
411
+ return { json: JSON.parse(line), rest };
412
+ }
413
+
414
+ function writeStdio(msg) {
415
+ const json = JSON.stringify(msg);
416
+ const body = Buffer.from(json, "utf8");
417
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
418
+ process.stdout.write(body);
419
+ }
420
+
421
+ export async function runCli(argv) {
422
+ const args = [...argv];
423
+ if (args.length === 0) {
246
424
  await runStdioProxy();
247
- return;
425
+ return 0;
248
426
  }
249
427
 
250
- const cmd = positionals[0] || "";
251
- if (flags.help && positionals.length === 0) {
252
- process.stdout.write(HELP);
253
- return;
428
+ const cmd = args[0];
429
+ if (cmd === "-V" || cmd === "--version" || cmd === "-v" || cmd === "version") {
430
+ out.write(`${packageVersion()}\n`);
431
+ return 0;
254
432
  }
255
- if (cmd === "help" || cmd === "--help") {
256
- process.stdout.write(HELP);
257
- return;
433
+ if (isHelpFlag(cmd) && args.length === 1) {
434
+ printHumanHelp(catalogTools());
435
+ return 0;
258
436
  }
259
- if (cmd === "update") {
260
- const ver = positionals[1] ? String(positionals[1]).replace(/^@/, "") : "latest";
261
- await new Promise((resolve, reject) => {
262
- const child = spawn("npm", ["i", "-g", `@ikeytz/maps-mcp@${ver}`], { stdio: "inherit" });
263
- child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`npm exit ${code}`))));
264
- });
265
- return;
437
+ if (cmd === "serve" || cmd === "proxy") {
438
+ await runStdioProxy();
439
+ return 0;
440
+ }
441
+ if (cmd === "update" || cmd === "upgrade") {
442
+ const spec = args[1] && !args[1].startsWith("-") ? args[1] : "latest";
443
+ const pkg = spec === "latest" ? "@ikeytz/maps-mcp@latest" : `@ikeytz/maps-mcp@${spec}`;
444
+ log.write(`Updating ${pkg}…\n`);
445
+ const r = spawnSync("npm", ["i", "-g", "--prefer-online", pkg], { stdio: "inherit", shell: false });
446
+ if (r.status !== 0) return r.status || 1;
447
+ log.write(`Done. New terminal → ikeytz-maps-mcp --version\n`);
448
+ return 0;
266
449
  }
267
450
  if (cmd === "list") {
268
- if (flags.global) {
269
- process.stdout.write(`${globalCliFlagsLines().join("\n")}\n`);
270
- return;
451
+ const rest = args.slice(1);
452
+ for (const a of rest) {
453
+ if (a !== "--global") {
454
+ log.write(`Usage: ikeytz-maps-mcp list [--global]\n`);
455
+ return 2;
456
+ }
271
457
  }
272
- process.stdout.write(`${toolNames().join("\n")}\n\n${globalCliFlagsLines().join("\n")}\n`);
273
- return;
458
+ if (rest.includes("--global")) {
459
+ out.write(`${globalCliFlagsLines().join("\n")}\n`);
460
+ return 0;
461
+ }
462
+ const rows = catalogTools().map((t) => ({
463
+ tool: t.name,
464
+ args: argHint(t) || "—",
465
+ example: exampleForTool(t.name),
466
+ }));
467
+ out.write([...formatToolsAsciiTable(rows), ...globalCliFlagsLines()].join("\n") + "\n");
468
+ return 0;
274
469
  }
275
- if (cmd === "describe") {
276
- const name = positionals[1];
277
- const tool = toolByName(name);
278
- if (!tool) {
279
- console.error(`unknown tool: ${name || "(fehlt)"}`);
280
- process.exitCode = 1;
281
- return;
470
+
471
+ let toolName;
472
+ let rest;
473
+ if (cmd === "describe" || cmd === "call") {
474
+ toolName = args[1];
475
+ rest = args.slice(2);
476
+ if (!toolName) {
477
+ log.write(`Usage: ikeytz-maps-mcp ${cmd} <toolName> …\n`);
478
+ return 2;
282
479
  }
283
- process.stdout.write(formatToolHelp(tool));
284
- return;
480
+ } else {
481
+ toolName = cmd;
482
+ rest = args.slice(1);
285
483
  }
286
484
 
287
- const toolName = cmd === "call" ? positionals[1] : cmd;
288
- const tool = toolByName(toolName);
289
- if (!tool) {
290
- console.error(`unknown command: ${cmd}\n`);
291
- process.stdout.write(HELP);
292
- process.exitCode = 1;
293
- return;
485
+ if (cmd === "describe" || rest.some(isHelpFlag)) {
486
+ const tool = toolByName(toolName);
487
+ if (!tool) {
488
+ log.write(`Unknown tool: ${toolName}\n`);
489
+ return 1;
490
+ }
491
+ out.write(`${formatDescribe(tool)}\n`);
492
+ return 0;
294
493
  }
295
- if (flags.help) {
296
- process.stdout.write(formatToolHelp(tool));
297
- return;
494
+
495
+ const wantRaw = rest.includes("--raw");
496
+ const showMcp = wantsShowMcpCommand(rest);
497
+ let callArgs;
498
+ try {
499
+ callArgs = parseToolArgs(stripCliDisplayFlags(rest.filter((a) => a !== "--raw")));
500
+ } catch (err) {
501
+ log.write(`${err.message || err}\n`);
502
+ return 2;
298
503
  }
299
- const args = argsFromFlags(flags);
300
- if (flags.showMcp) {
301
- process.stdout.write(showMcpCommand(toolName, args));
302
- return;
504
+
505
+ if (showMcp) {
506
+ out.write(`${formatMcpCurlHeading(toolName, callArgs)}\n`);
507
+ return 0;
303
508
  }
304
- const rpc = await callTool(toolName, args);
305
- printPayload(rpc, flags.raw);
509
+
510
+ const rpc = await callTool(toolName, callArgs);
511
+ printCallResult(rpc, { raw: wantRaw });
512
+ if (rpc?.result?.isError || rpc?.error) process.exitCode = 1;
513
+ return process.exitCode || 0;
306
514
  }
515
+
516
+ /** @deprecated use runCli — kept for older bin */
517
+ export const main = runCli;