@ikeytz/maps-mcp 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - Erstes lokales Paket MAPS-M5 (C1/C2). Remote nur `https://maps.ikeytz.com/mcp`.
6
+ - Publish npm = MAPS-E3, noch nicht.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) Mahmud Reza Kashani / ikeytz.
2
+
3
+ LicenseRef-ikeytz-terms — nicht OSI, kein Training.
4
+ Texte und Karten-Darstellung: siehe https://www.ikeytz.com/urheberrecht
5
+ und https://maps.ikeytz.com/llms-urheberrecht.txt
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @ikeytz/maps-mcp
2
+
3
+ CLI + stdio-Proxy für **maps.ikeytz.com/mcp**. Anderes Paket als `@ikeytz/mcp` (www).
4
+
5
+ | | |
6
+ |--|--|
7
+ | Bin | `ikeytz-maps-mcp` |
8
+ | `mcpName` | `com.ikeytz/maps` |
9
+ | Remote | `https://maps.ikeytz.com/mcp` |
10
+ | Protocol | `2025-11-25` |
11
+
12
+ **Publish (npm) = MAPS-E3**, noch nicht. Lokal aus diesem Repo:
13
+
14
+ ```bash
15
+ node packages/maps-mcp/bin/ikeytz-maps-mcp.js --version
16
+ node packages/maps-mcp/bin/ikeytz-maps-mcp.js list
17
+ node packages/maps-mcp/bin/ikeytz-maps-mcp.js get_service_area --slug=pattonville
18
+ ```
19
+
20
+ Nach Publish:
21
+
22
+ ```bash
23
+ npm i -g @ikeytz/maps-mcp
24
+ ikeytz-maps-mcp --version
25
+ npx -y @ikeytz/maps-mcp help
26
+ ```
27
+
28
+ Env (nicht `IKEYTZ_MCP_URL`):
29
+
30
+ | Variable | Default |
31
+ |----------|---------|
32
+ | `IKEYTZ_MAPS_MCP_URL` | `https://maps.ikeytz.com/mcp` |
33
+ | `IKEYTZ_MAPS_MCP_PROTOCOL` | `2025-11-25` |
34
+ | `IKEYTZ_MAPS_MCP_TIMEOUT_MS` | `15000` |
35
+
36
+ Ohne Args → stdio (Cursor/Claude):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "ikeytz-maps": {
42
+ "command": "npx",
43
+ "args": ["-y", "@ikeytz/maps-mcp"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ Lokal vor Publish: `"command": "node"`, `"args": ["…/packages/maps-mcp/bin/ikeytz-maps-mcp.js"]`.
50
+
51
+ www bleibt zweiter Eintrag → `@ikeytz/mcp` / `https://www.ikeytz.com/mcp`.
52
+
53
+ Falsch: `ikeytz-mcp get_service_area --slug=pattonville` (das ist www).
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.js";
3
+
4
+ main(process.argv.slice(2)).catch((err) => {
5
+ console.error(err?.message || err);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@ikeytz/maps-mcp",
3
+ "version": "1.0.0",
4
+ "mcpName": "com.ikeytz/maps",
5
+ "description": "Read/Link MCP CLI for maps.ikeytz.com. Not @ikeytz/mcp.",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "ikeytz-maps-mcp": "bin/ikeytz-maps-mcp.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "LicenseRef-ikeytz-terms",
17
+ "files": [
18
+ "bin",
19
+ "src",
20
+ "LICENSE",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://vmd199268.contaboserver.net/ikeytz-geo/ikeytz-geo-stack.git",
27
+ "directory": "packages/maps-mcp"
28
+ }
29
+ }
package/src/cli.js ADDED
@@ -0,0 +1,225 @@
1
+ import { spawn } from "node:child_process";
2
+ import { callTool, mapsMcpUrl, mapsRpc, packageVersion, showMcpCommand, toolByName, toolNames } from "./proxy.js";
3
+
4
+ const TOOL_FLAGS = ["slug", "plz", "name", "which", "offset", "limit"];
5
+
6
+ const HELP = `ikeytz-maps-mcp ${packageVersion()}
7
+ Remote: ${mapsMcpUrl()}
8
+ Nur maps. Nicht @ikeytz/mcp / www.
9
+
10
+ Ohne Args stdio-Proxy (Cursor/Claude)
11
+ help | --help diese Hilfe
12
+ --version Version
13
+ update npm i -g @ikeytz/maps-mcp@latest
14
+ list 7 Tools (offline)
15
+ describe <tool> Schema (offline)
16
+ call <tool> … wie <tool> …
17
+ <tool> Payload von maps /mcp
18
+ <tool> -h Args (offline)
19
+ <tool> --show-mcp-command | --show-mcp-befehl
20
+ <tool> --raw volles JSON-RPC
21
+ <tool> --json '{…}'
22
+
23
+ Tools:
24
+ list_service_areas
25
+ get_service_area --slug=… | --plz=… | --name=…
26
+ get_geo_office
27
+ get_contact
28
+ get_discovery --which=llms|llms-orte-geo|ai-plugin|…
29
+ get_llms_mcp_server
30
+ get_llms_mcp_web
31
+ `;
32
+
33
+ function parseArgv(argv) {
34
+ const flags = {};
35
+ const positionals = [];
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === "--raw") flags.raw = true;
39
+ else if (a === "-h" || a === "--help") flags.help = true;
40
+ else if (a === "--version") flags.version = true;
41
+ else if (a === "--show-mcp-command" || a === "--show-mcp-befehl") flags.showMcp = true;
42
+ else if (a === "--json") flags.json = argv[++i] || "";
43
+ else if (a.startsWith("--json=")) flags.json = a.slice(7);
44
+ else if (a.startsWith("--")) {
45
+ const eq = a.indexOf("=");
46
+ if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
47
+ else {
48
+ const key = a.slice(2);
49
+ const next = argv[i + 1];
50
+ if (next && !next.startsWith("-")) {
51
+ flags[key] = next;
52
+ i += 1;
53
+ } else flags[key] = true;
54
+ }
55
+ } else positionals.push(a);
56
+ }
57
+ return { flags, positionals };
58
+ }
59
+
60
+ function argsFromFlags(flags) {
61
+ const args = {};
62
+ if (flags.json) Object.assign(args, JSON.parse(flags.json));
63
+ for (const key of TOOL_FLAGS) {
64
+ if (flags[key] != null && flags[key] !== true) {
65
+ args[key] = key === "offset" || key === "limit" ? Number(flags[key]) : String(flags[key]);
66
+ }
67
+ }
68
+ return args;
69
+ }
70
+
71
+ function printPayload(rpc, raw) {
72
+ if (raw) {
73
+ process.stdout.write(`${JSON.stringify(rpc, null, 2)}\n`);
74
+ return;
75
+ }
76
+ const data = rpc?.result?.structuredContent ?? rpc?.result ?? rpc;
77
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
78
+ }
79
+
80
+ function readStdioMessage(buf) {
81
+ const text = buf.toString("utf8");
82
+ const headerEnd = text.indexOf("\r\n\r\n");
83
+ const altEnd = text.indexOf("\n\n");
84
+ if (headerEnd >= 0 || (altEnd >= 0 && /content-length:/i.test(text.slice(0, altEnd + 2)))) {
85
+ const split = headerEnd >= 0 ? headerEnd : altEnd;
86
+ const sep = headerEnd >= 0 ? 4 : 2;
87
+ const header = text.slice(0, split);
88
+ const m = header.match(/content-length:\s*(\d+)/i);
89
+ if (!m) return null;
90
+ const len = Number(m[1]);
91
+ const start = split + sep;
92
+ const body = Buffer.from(text, "utf8").subarray(start);
93
+ if (body.length < len) return null;
94
+ const json = JSON.parse(body.subarray(0, len).toString("utf8"));
95
+ return { json, rest: body.subarray(len) };
96
+ }
97
+ const nl = text.indexOf("\n");
98
+ if (nl < 0) return null;
99
+ const line = text.slice(0, nl).trim();
100
+ const rest = Buffer.from(text.slice(nl + 1), "utf8");
101
+ if (!line) return { json: null, rest };
102
+ return { json: JSON.parse(line), rest };
103
+ }
104
+
105
+ function writeStdio(msg) {
106
+ const json = JSON.stringify(msg);
107
+ const body = Buffer.from(json, "utf8");
108
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
109
+ process.stdout.write(body);
110
+ }
111
+
112
+ async function runStdioProxy() {
113
+ let sessionId = "";
114
+ let buf = Buffer.alloc(0);
115
+ process.stdin.on("data", (chunk) => {
116
+ buf = Buffer.concat([buf, chunk]);
117
+ (async () => {
118
+ while (true) {
119
+ let parsed;
120
+ try {
121
+ parsed = readStdioMessage(buf);
122
+ } catch (err) {
123
+ writeStdio({ jsonrpc: "2.0", id: null, error: { code: -32700, message: String(err.message || err) } });
124
+ buf = Buffer.alloc(0);
125
+ break;
126
+ }
127
+ if (!parsed) break;
128
+ buf = parsed.rest;
129
+ if (!parsed.json) continue;
130
+ const msg = parsed.json;
131
+ const method = msg.method;
132
+ const id = msg.id;
133
+ try {
134
+ if (method === "initialize") {
135
+ const out = await mapsRpc("initialize", msg.params || {}, { id: id ?? 1 });
136
+ sessionId = out.sessionId || sessionId;
137
+ writeStdio(out.json);
138
+ } else if (method === "notifications/initialized" || method === "initialized") {
139
+ const out = await mapsRpc("notifications/initialized", msg.params || {}, {
140
+ notification: true,
141
+ sessionId,
142
+ });
143
+ sessionId = out.sessionId || sessionId;
144
+ if (id !== undefined) writeStdio({ jsonrpc: "2.0", id, result: {} });
145
+ } else if (id === undefined) {
146
+ await mapsRpc(method, msg.params || {}, { notification: true, sessionId });
147
+ } else {
148
+ const out = await mapsRpc(method, msg.params || {}, { id, sessionId });
149
+ sessionId = out.sessionId || sessionId;
150
+ writeStdio(out.json);
151
+ }
152
+ } catch (err) {
153
+ if (id !== undefined) {
154
+ writeStdio({ jsonrpc: "2.0", id, error: { code: -32000, message: String(err.message || err) } });
155
+ }
156
+ }
157
+ }
158
+ })();
159
+ });
160
+ }
161
+
162
+ export async function main(argv) {
163
+ const { flags, positionals } = parseArgv(argv);
164
+
165
+ if (flags.version && positionals.length === 0) {
166
+ process.stdout.write(`${packageVersion()}\n`);
167
+ return;
168
+ }
169
+ if (argv.length === 0) {
170
+ await runStdioProxy();
171
+ return;
172
+ }
173
+
174
+ const cmd = positionals[0] || "";
175
+ if (flags.help && positionals.length === 0) {
176
+ process.stdout.write(HELP);
177
+ return;
178
+ }
179
+ if (cmd === "help" || cmd === "--help") {
180
+ process.stdout.write(HELP);
181
+ return;
182
+ }
183
+ if (cmd === "update") {
184
+ await new Promise((resolve, reject) => {
185
+ const child = spawn("npm", ["i", "-g", "@ikeytz/maps-mcp@latest"], { stdio: "inherit" });
186
+ child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`npm exit ${code}`))));
187
+ });
188
+ return;
189
+ }
190
+ if (cmd === "list") {
191
+ process.stdout.write(`${toolNames().join("\n")}\n`);
192
+ return;
193
+ }
194
+ if (cmd === "describe") {
195
+ const name = positionals[1];
196
+ const tool = toolByName(name);
197
+ if (!tool) {
198
+ console.error(`unknown tool: ${name || "(fehlt)"}`);
199
+ process.exitCode = 1;
200
+ return;
201
+ }
202
+ process.stdout.write(`${JSON.stringify(tool, null, 2)}\n`);
203
+ return;
204
+ }
205
+
206
+ const toolName = cmd === "call" ? positionals[1] : cmd;
207
+ const tool = toolByName(toolName);
208
+ if (!tool) {
209
+ console.error(`unknown command: ${cmd}\n`);
210
+ process.stdout.write(HELP);
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ if (flags.help) {
215
+ process.stdout.write(`${tool.name}\n${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}\n`);
216
+ return;
217
+ }
218
+ const args = argsFromFlags(flags);
219
+ if (flags.showMcp) {
220
+ process.stdout.write(showMcpCommand(toolName, args));
221
+ return;
222
+ }
223
+ const rpc = await callTool(toolName, args);
224
+ printPayload(rpc, flags.raw);
225
+ }
package/src/proxy.js ADDED
@@ -0,0 +1,123 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
6
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
7
+ export const CATALOG = JSON.parse(readFileSync(join(root, "src/tools-catalog.json"), "utf8"));
8
+
9
+ export const DEFAULT_URL = "https://maps.ikeytz.com/mcp";
10
+ export const DEFAULT_PROTOCOL = "2025-11-25";
11
+
12
+ export function mapsMcpUrl() {
13
+ return process.env.IKEYTZ_MAPS_MCP_URL || DEFAULT_URL;
14
+ }
15
+
16
+ export function mapsMcpProtocol() {
17
+ return process.env.IKEYTZ_MAPS_MCP_PROTOCOL || DEFAULT_PROTOCOL;
18
+ }
19
+
20
+ export function mapsMcpTimeoutMs() {
21
+ const n = Number(process.env.IKEYTZ_MAPS_MCP_TIMEOUT_MS || 15000);
22
+ return Number.isFinite(n) && n > 0 ? n : 15000;
23
+ }
24
+
25
+ export function packageVersion() {
26
+ return pkg.version;
27
+ }
28
+
29
+ export function toolByName(name) {
30
+ return (CATALOG.tools || []).find((t) => t.name === name) || null;
31
+ }
32
+
33
+ export function toolNames() {
34
+ return (CATALOG.tools || []).map((t) => t.name);
35
+ }
36
+
37
+ export async function mapsRpc(method, params, { id = 1, sessionId, notification = false } = {}) {
38
+ const url = mapsMcpUrl();
39
+ if (url.includes("geo.ikeytz.com") || url.includes("www.ikeytz.com/mcp")) {
40
+ throw new Error("IKEYTZ_MAPS_MCP_URL muss maps.ikeytz.com/mcp sein");
41
+ }
42
+ const body = notification
43
+ ? { jsonrpc: "2.0", method, params: params || {} }
44
+ : { jsonrpc: "2.0", id, method, params: params || {} };
45
+ const headers = {
46
+ "Content-Type": "application/json",
47
+ Accept: "application/json, text/event-stream",
48
+ "MCP-Protocol-Version": mapsMcpProtocol(),
49
+ };
50
+ if (sessionId) headers["Mcp-Session-Id"] = sessionId;
51
+ const res = await fetch(url, {
52
+ method: "POST",
53
+ headers,
54
+ body: JSON.stringify(body),
55
+ signal: AbortSignal.timeout(mapsMcpTimeoutMs()),
56
+ });
57
+ const nextSession = res.headers.get("mcp-session-id") || sessionId || "";
58
+ if (notification) {
59
+ return { sessionId: nextSession, status: res.status, json: null };
60
+ }
61
+ const json = await res.json();
62
+ return { sessionId: nextSession, status: res.status, json };
63
+ }
64
+
65
+ export async function withSession(fn) {
66
+ const init = await mapsRpc("initialize", {
67
+ protocolVersion: mapsMcpProtocol(),
68
+ capabilities: {},
69
+ clientInfo: { name: "ikeytz-maps-mcp", version: packageVersion() },
70
+ });
71
+ if (init.json?.error) throw new Error(init.json.error.message || "initialize failed");
72
+ const sessionId = init.sessionId;
73
+ await mapsRpc("notifications/initialized", {}, { notification: true, sessionId });
74
+ return fn(sessionId);
75
+ }
76
+
77
+ export async function callTool(name, args = {}) {
78
+ return withSession(async (sessionId) => {
79
+ const out = await mapsRpc("tools/call", { name, arguments: args }, { id: 2, sessionId });
80
+ if (out.json?.error) {
81
+ const err = new Error(out.json.error.message || "tools/call failed");
82
+ err.rpc = out.json;
83
+ throw err;
84
+ }
85
+ return out.json;
86
+ });
87
+ }
88
+
89
+ export function showMcpCommand(name, args = {}) {
90
+ const url = mapsMcpUrl();
91
+ const proto = mapsMcpProtocol();
92
+ const payload = JSON.stringify({
93
+ jsonrpc: "2.0",
94
+ id: 2,
95
+ method: "tools/call",
96
+ params: { name, arguments: args },
97
+ });
98
+ return `# 1a) Session-ID holen
99
+ SID=$(curl -sS -D - -o /tmp/ikeytz-maps-mcp-init.json -X POST '${url}' \\
100
+ -H 'Content-Type: application/json' \\
101
+ -H 'Accept: application/json' \\
102
+ -H 'MCP-Protocol-Version: ${proto}' \\
103
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"${proto}","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \\
104
+ | awk -F': ' 'tolower($1)=="mcp-session-id"{gsub(/\\r/,"",$2); print $2; exit}')
105
+ echo "SID=$SID"
106
+
107
+ # 1b) initialized
108
+ curl -sS -X POST '${url}' \\
109
+ -H 'Content-Type: application/json' \\
110
+ -H 'Accept: application/json' \\
111
+ -H 'MCP-Protocol-Version: ${proto}' \\
112
+ -H "Mcp-Session-Id: $SID" \\
113
+ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
114
+
115
+ # 2) Tool
116
+ curl -sS -X POST '${url}' \\
117
+ -H 'Content-Type: application/json' \\
118
+ -H 'Accept: application/json' \\
119
+ -H 'MCP-Protocol-Version: ${proto}' \\
120
+ -H "Mcp-Session-Id: $SID" \\
121
+ -d '${payload}'
122
+ `;
123
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "tools": [
3
+ {
4
+ "name": "list_service_areas",
5
+ "description": "Alle 52 Einsatzgebiete auf maps.ikeytz.com (Punkte, keine Polygone).",
6
+ "inputSchema": {
7
+ "type": "object",
8
+ "properties": {}
9
+ }
10
+ },
11
+ {
12
+ "name": "get_service_area",
13
+ "description": "Ein Einsatzgebiet. Lookup: slug → plz (erster Treffer) → name (Substring).",
14
+ "inputSchema": {
15
+ "type": "object",
16
+ "properties": {
17
+ "slug": {
18
+ "type": "string"
19
+ },
20
+ "plz": {
21
+ "type": "string"
22
+ },
23
+ "name": {
24
+ "type": "string"
25
+ }
26
+ }
27
+ }
28
+ },
29
+ {
30
+ "name": "get_geo_office",
31
+ "description": "Gewerbesitz Ludwigsburg. Kein Walk-in.",
32
+ "inputSchema": {
33
+ "type": "object",
34
+ "properties": {}
35
+ }
36
+ },
37
+ {
38
+ "name": "get_contact",
39
+ "description": "NAP: Handy, Festnetz, Fax, E-Mail, WhatsApp, Adresse.",
40
+ "inputSchema": {
41
+ "type": "object",
42
+ "properties": {}
43
+ }
44
+ },
45
+ {
46
+ "name": "get_discovery",
47
+ "description": "1:1 Spiegel einer maps-Discovery-Datei (llms, robots, plugin, sitemap, …).",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {
51
+ "which": {
52
+ "type": "string",
53
+ "enum": [
54
+ "llms",
55
+ "llms-full",
56
+ "llms-index",
57
+ "llms-keywords",
58
+ "llms-serp",
59
+ "llms-impressum-kontakt",
60
+ "llms-orte-geo",
61
+ "llms-urheberrecht",
62
+ "llms-copyright",
63
+ "llms-mcp-server",
64
+ "llms-mcp-web",
65
+ "robots",
66
+ "sitemap-txt",
67
+ "sitemap-xml",
68
+ "ai-txt",
69
+ "ai-plugin",
70
+ "answer-engine",
71
+ "ai-catalog"
72
+ ]
73
+ },
74
+ "offset": {
75
+ "type": "integer"
76
+ },
77
+ "limit": {
78
+ "type": "integer"
79
+ }
80
+ },
81
+ "required": [
82
+ "which"
83
+ ]
84
+ }
85
+ },
86
+ {
87
+ "name": "get_llms_mcp_server",
88
+ "description": "Katalog llms-mcp-server.txt",
89
+ "inputSchema": {
90
+ "type": "object",
91
+ "properties": {}
92
+ }
93
+ },
94
+ {
95
+ "name": "get_llms_mcp_web",
96
+ "description": "Katalog llms-mcp-web.txt",
97
+ "inputSchema": {
98
+ "type": "object",
99
+ "properties": {}
100
+ }
101
+ }
102
+ ]
103
+ }