|
1 | 1 | #!/usr/bin/env node |
2 | 2 |
|
3 | | -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 3 | +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; |
4 | 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; |
5 | 5 | import { z } from "zod"; |
6 | 6 |
|
@@ -1160,10 +1160,98 @@ import path from "path"; |
1160 | 1160 | })(); |
1161 | 1161 |
|
1162 | 1162 | // ═══════════════════════════════════════════════════════════════════════ |
1163 | | -// START |
| 1163 | +// RESOURCES — code navigation via URI templates (MCP-native) |
| 1164 | +// crosspad://symbols/{repo}/{symbol} — resolve a single symbol's definitions |
| 1165 | +// in a single repo without spending a tool call. Repo "all" searches every |
| 1166 | +// detected repo. listCallback is undefined (cannot enumerate every symbol); |
| 1167 | +// clients must construct concrete URIs. |
1164 | 1168 | // ═══════════════════════════════════════════════════════════════════════ |
1165 | 1169 |
|
| 1170 | +server.registerResource( |
| 1171 | + "crosspad-symbol", |
| 1172 | + new ResourceTemplate("crosspad://symbols/{repo}/{symbol}", { list: undefined }), |
| 1173 | + { |
| 1174 | + description: "Resolve a single symbol by repo+name. URI: crosspad://symbols/<repo>/<symbol>. <repo> is one of: crosspad-core, crosspad-gui, crosspad-pc, platform-idf, ESP32-S3, or 'all'. Returns JSON with matching definition(s) (class/function/macro/enum/typedef). For substring/wildcard search, use the crosspad_search_symbols tool.", |
| 1175 | + mimeType: "application/json", |
| 1176 | + }, |
| 1177 | + async (uri, variables) => { |
| 1178 | + const repo = decodeURIComponent(String(Array.isArray(variables.repo) ? variables.repo[0] : variables.repo ?? "")).trim(); |
| 1179 | + const symbol = decodeURIComponent(String(Array.isArray(variables.symbol) ? variables.symbol[0] : variables.symbol ?? "")).trim(); |
| 1180 | + if (!repo || !symbol) { |
| 1181 | + return { |
| 1182 | + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "URI must be crosspad://symbols/<repo>/<symbol>" }, null, 2) }], |
| 1183 | + }; |
| 1184 | + } |
| 1185 | + const reposScope = repo === "all" ? ["all"] : [repo]; |
| 1186 | + const result = crosspadSearchSymbols(symbol, "all", reposScope, 50, 0); |
| 1187 | + return { |
| 1188 | + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(result, null, 2) }], |
| 1189 | + }; |
| 1190 | + } |
| 1191 | +); |
| 1192 | + |
| 1193 | +// ═══════════════════════════════════════════════════════════════════════ |
| 1194 | +// START — stdio (default) or HTTP (--http <port>) |
| 1195 | +// HTTP transport is opt-in via CLI flag for remote dev boxes / browsers. |
| 1196 | +// Stateful sessions: each initialize gets a session ID; subsequent requests |
| 1197 | +// must echo it. Single shared transport multiplexes sessions internally. |
| 1198 | +// ═══════════════════════════════════════════════════════════════════════ |
| 1199 | + |
| 1200 | +function parseHttpPort(argv: string[]): number | null { |
| 1201 | + for (let i = 0; i < argv.length; i++) { |
| 1202 | + const a = argv[i]; |
| 1203 | + if (a === "--http") { |
| 1204 | + const next = argv[i + 1]; |
| 1205 | + if (!next) return 3000; |
| 1206 | + const n = parseInt(next, 10); |
| 1207 | + return Number.isFinite(n) && n > 0 && n < 65536 ? n : NaN as unknown as number; |
| 1208 | + } |
| 1209 | + if (a.startsWith("--http=")) { |
| 1210 | + const n = parseInt(a.slice("--http=".length), 10); |
| 1211 | + return Number.isFinite(n) && n > 0 && n < 65536 ? n : NaN as unknown as number; |
| 1212 | + } |
| 1213 | + } |
| 1214 | + return null; |
| 1215 | +} |
| 1216 | + |
1166 | 1217 | async function main() { |
| 1218 | + const httpPort = parseHttpPort(process.argv.slice(2)); |
| 1219 | + if (httpPort !== null) { |
| 1220 | + if (Number.isNaN(httpPort)) { |
| 1221 | + console.error("Invalid --http port (must be 1..65535)"); |
| 1222 | + process.exit(1); |
| 1223 | + } |
| 1224 | + const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js"); |
| 1225 | + const { createServer } = await import("http"); |
| 1226 | + const { randomUUID } = await import("crypto"); |
| 1227 | + |
| 1228 | + const transport = new StreamableHTTPServerTransport({ |
| 1229 | + sessionIdGenerator: () => randomUUID(), |
| 1230 | + }); |
| 1231 | + await server.connect(transport); |
| 1232 | + |
| 1233 | + const httpServer = createServer((req, res) => { |
| 1234 | + const pathname = (req.url ?? "/").split("?")[0]; |
| 1235 | + if (pathname !== "/mcp") { |
| 1236 | + res.writeHead(404, { "Content-Type": "text/plain" }); |
| 1237 | + res.end("Not Found — MCP endpoint is at /mcp"); |
| 1238 | + return; |
| 1239 | + } |
| 1240 | + transport.handleRequest(req, res).catch((e) => { |
| 1241 | + console.error("MCP HTTP request failed:", e); |
| 1242 | + if (!res.headersSent) { |
| 1243 | + res.writeHead(500, { "Content-Type": "text/plain" }); |
| 1244 | + res.end("Internal error"); |
| 1245 | + } |
| 1246 | + }); |
| 1247 | + }); |
| 1248 | + |
| 1249 | + httpServer.listen(httpPort, () => { |
| 1250 | + console.error(`crosspad-mcp HTTP transport listening on http://localhost:${httpPort}/mcp`); |
| 1251 | + }); |
| 1252 | + return; |
| 1253 | + } |
| 1254 | + |
1167 | 1255 | const transport = new StdioServerTransport(); |
1168 | 1256 | await server.connect(transport); |
1169 | 1257 | } |
|
0 commit comments