@topolo/mcp 0.4.9 → 0.5.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/dist/dispatch.js +28 -22
- package/dist/http.d.ts +63 -0
- package/dist/http.js +16462 -0
- package/dist/index.js +167 -158
- package/package.json +17 -13
package/dist/dispatch.js
CHANGED
|
@@ -22,13 +22,13 @@ function hasScope(set, required) {
|
|
|
22
22
|
if (set.permissions.includes("*")) return true;
|
|
23
23
|
const [servicePart, actionPart] = splitPermission(required);
|
|
24
24
|
if (!servicePart) return set.permissions.includes(required);
|
|
25
|
-
const [
|
|
25
|
+
const [appId, resource] = splitService(servicePart);
|
|
26
26
|
const candidates = new Set([
|
|
27
27
|
required,
|
|
28
28
|
"*",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
appId ? `${appId}.*` : null,
|
|
30
|
+
appId && resource && actionPart ? `${appId}.${resource}:*` : null,
|
|
31
|
+
appId && resource && actionPart ? `${appId}.${resource}.${actionPart}` : null,
|
|
32
32
|
resource && actionPart ? `${resource}:*` : null,
|
|
33
33
|
resource && actionPart ? `${resource}.${actionPart}` : null,
|
|
34
34
|
resource && actionPart ? `${resource}:${actionPart}` : null
|
|
@@ -80,10 +80,10 @@ var TOOLS = [
|
|
|
80
80
|
},
|
|
81
81
|
handler: async (topolo) => {
|
|
82
82
|
const services = (await topolo.client.listServices()).map((entry) => {
|
|
83
|
-
const id =
|
|
83
|
+
const id = displayAppId(entry);
|
|
84
84
|
return {
|
|
85
85
|
id,
|
|
86
|
-
|
|
86
|
+
appId: entry.appId,
|
|
87
87
|
slug: entry.slug,
|
|
88
88
|
name: entry.name,
|
|
89
89
|
apiBaseUrl: entry.apiBaseUrl,
|
|
@@ -110,7 +110,7 @@ var TOOLS = [
|
|
|
110
110
|
properties: {
|
|
111
111
|
service: {
|
|
112
112
|
type: "string",
|
|
113
|
-
description: "Optional
|
|
113
|
+
description: "Optional app id, slug, or name filter."
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
additionalProperties: false
|
|
@@ -139,7 +139,7 @@ var TOOLS = [
|
|
|
139
139
|
{
|
|
140
140
|
name: "topolo_get_application",
|
|
141
141
|
title: "Describe one Topolo platform application",
|
|
142
|
-
description: "Returns one live application catalog entry (
|
|
142
|
+
description: "Returns one live application catalog entry (app id, slug, name, API URL, launch URL, status). Accepts the app id, slug, or name. Resolved against the live registry so newly-registered apps work without a client rebuild.",
|
|
143
143
|
requiredScopes: [],
|
|
144
144
|
destructive: false,
|
|
145
145
|
inputSchema: {
|
|
@@ -147,7 +147,7 @@ var TOOLS = [
|
|
|
147
147
|
properties: {
|
|
148
148
|
application: {
|
|
149
149
|
type: "string",
|
|
150
|
-
description: "Application identifier:
|
|
150
|
+
description: "Application identifier: app id, slug, or name from topolo_list_applications."
|
|
151
151
|
}
|
|
152
152
|
},
|
|
153
153
|
required: ["application"],
|
|
@@ -161,10 +161,10 @@ var TOOLS = [
|
|
|
161
161
|
const needle = application.trim().toLowerCase();
|
|
162
162
|
const catalog = await topolo.client.listServices();
|
|
163
163
|
const match = catalog.find(
|
|
164
|
-
(e) => e.
|
|
164
|
+
(e) => e.appId.toLowerCase() === needle || e.slug && e.slug.toLowerCase() === needle || e.name && e.name.toLowerCase() === needle
|
|
165
165
|
);
|
|
166
166
|
if (!match) {
|
|
167
|
-
const known = catalog.map((e) => e.slug || e.
|
|
167
|
+
const known = catalog.map((e) => e.slug || e.appId).sort().filter((value) => typeof value === "string" && value.length > 0);
|
|
168
168
|
throw new Error(
|
|
169
169
|
`Unknown application "${application}". ${formatKnownValues(known, "applications")}`
|
|
170
170
|
);
|
|
@@ -347,14 +347,20 @@ async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
|
347
347
|
const actionTools = actionsToToolDefs(actions);
|
|
348
348
|
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
349
349
|
}
|
|
350
|
+
function effectiveAgentAccess(a) {
|
|
351
|
+
if (a.agentAccess) return a.agentAccess;
|
|
352
|
+
if (a.destructive) return "off";
|
|
353
|
+
if (a.readOnly && !a.requiresConfirmation) return "auto";
|
|
354
|
+
return "confirm";
|
|
355
|
+
}
|
|
350
356
|
function actionsToToolDefs(actions) {
|
|
351
|
-
return actions.map((action) => ({
|
|
357
|
+
return actions.map((action) => ({ action, access: effectiveAgentAccess(action) })).filter(({ access }) => access !== "off").map(({ action, access }) => ({
|
|
352
358
|
name: action.toolName || toolNameForAction(action),
|
|
353
359
|
title: action.title,
|
|
354
360
|
description: action.description,
|
|
355
361
|
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
356
|
-
destructive:
|
|
357
|
-
inputSchema: actionInputSchema(action),
|
|
362
|
+
destructive: access === "confirm",
|
|
363
|
+
inputSchema: actionInputSchema(action, access),
|
|
358
364
|
handler: async (topolo, args) => {
|
|
359
365
|
const confirm = Boolean(args["confirm"]);
|
|
360
366
|
const input = { ...args };
|
|
@@ -364,11 +370,11 @@ function actionsToToolDefs(actions) {
|
|
|
364
370
|
}));
|
|
365
371
|
}
|
|
366
372
|
function toolNameForAction(action) {
|
|
367
|
-
const service = (action.serviceSlug || action.
|
|
373
|
+
const service = (action.serviceSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
368
374
|
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
369
375
|
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
370
376
|
}
|
|
371
|
-
function actionInputSchema(action) {
|
|
377
|
+
function actionInputSchema(action, access = "auto") {
|
|
372
378
|
const schema = {
|
|
373
379
|
type: "object",
|
|
374
380
|
properties: {},
|
|
@@ -376,7 +382,7 @@ function actionInputSchema(action) {
|
|
|
376
382
|
...action.inputSchema
|
|
377
383
|
};
|
|
378
384
|
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
379
|
-
if (
|
|
385
|
+
if (access === "confirm") {
|
|
380
386
|
properties.confirm = {
|
|
381
387
|
type: "boolean",
|
|
382
388
|
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
@@ -425,18 +431,18 @@ function statusesAtOrWorse(threshold) {
|
|
|
425
431
|
if (threshold === "needs_review") return ["missing", "needs_review"];
|
|
426
432
|
return ["missing", "needs_review", "partial"];
|
|
427
433
|
}
|
|
428
|
-
function
|
|
429
|
-
for (const value of [entry.slug, entry.name, entry.
|
|
434
|
+
function displayAppId(entry) {
|
|
435
|
+
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
430
436
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
431
|
-
for (const candidate of
|
|
437
|
+
for (const candidate of appIdCandidates(value)) {
|
|
432
438
|
if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
|
|
433
439
|
return candidate;
|
|
434
440
|
}
|
|
435
441
|
}
|
|
436
442
|
}
|
|
437
|
-
return
|
|
443
|
+
return appIdCandidates(entry.slug || entry.appId)[0] || entry.appId;
|
|
438
444
|
}
|
|
439
|
-
function
|
|
445
|
+
function appIdCandidates(value) {
|
|
440
446
|
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
441
447
|
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
442
448
|
return [.../* @__PURE__ */ new Set([
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
2
|
+
import { TopoloCredential, Topolo } from '@topolo/sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hosted, multi-tenant MCP Streamable HTTP transport for the Topolo platform.
|
|
6
|
+
*
|
|
7
|
+
* stdio (index.ts) binds ONE env credential to the whole process. The hosted
|
|
8
|
+
* HTTP transport is the opposite: every request carries its OWN caller
|
|
9
|
+
* credential (Authorization: Bearer <jwt> or X-Api-Key: <key>). We build a
|
|
10
|
+
* per-request Topolo SDK client from THAT credential, introspect it to resolve
|
|
11
|
+
* the caller's scopes, and wire a per-session MCP server via buildServer — the
|
|
12
|
+
* exact same scope-gated tool list + dispatch path used by stdio. There is no
|
|
13
|
+
* process-wide credential fallback; a request with no/invalid credential is
|
|
14
|
+
* rejected with 401.
|
|
15
|
+
*
|
|
16
|
+
* Protocol: implemented by the SDK's WebStandardStreamableHTTPServerTransport,
|
|
17
|
+
* which speaks the MCP Streamable HTTP spec (POST JSON-RPC -> JSON or SSE,
|
|
18
|
+
* Mcp-Session-Id continuity, GET SSE stream, DELETE to terminate) over pure
|
|
19
|
+
* Web-Standard Request/Response — so it runs unchanged in a Cloudflare Worker.
|
|
20
|
+
*/
|
|
21
|
+
interface TopoloMcpHttpOptions {
|
|
22
|
+
/** Path the MCP endpoint is mounted at. Defaults to '/mcp'. */
|
|
23
|
+
path?: string;
|
|
24
|
+
/** Per-service URL overrides (e.g. staging). Passed through to createTopolo. */
|
|
25
|
+
serviceUrls?: Record<string, string>;
|
|
26
|
+
/** Per-service TopoloAuth app-id overrides. Passed through to createTopolo. */
|
|
27
|
+
appIds?: Record<string, string>;
|
|
28
|
+
/** Human-readable agent label for audit logs (forwarded to the SDK). */
|
|
29
|
+
agentName?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Injected fetch passed to the default createTopolo factory. Defaults to the
|
|
32
|
+
* runtime global fetch. Useful in a Worker (to forward `env`-bound fetch) and
|
|
33
|
+
* in tests. Ignored when `createClient` is supplied.
|
|
34
|
+
*/
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
/**
|
|
37
|
+
* Build the per-request SDK client. Injectable so tests can supply a fake
|
|
38
|
+
* Topolo without real network I/O. Defaults to createTopolo. The credential
|
|
39
|
+
* is always the per-request caller credential — never an env credential.
|
|
40
|
+
*/
|
|
41
|
+
createClient?: (credential: TopoloCredential) => Topolo;
|
|
42
|
+
}
|
|
43
|
+
interface McpSession {
|
|
44
|
+
transport: WebStandardStreamableHTTPServerTransport;
|
|
45
|
+
/** Fingerprint of the credential that created this session. */
|
|
46
|
+
fingerprint: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a Cloudflare-Worker-compatible fetch handler that serves the Topolo
|
|
50
|
+
* MCP over Streamable HTTP. Mount it as the Worker's default export, e.g.:
|
|
51
|
+
*
|
|
52
|
+
* const handler = createTopoloMcpHttpHandler();
|
|
53
|
+
* export default { fetch: handler.fetch };
|
|
54
|
+
*/
|
|
55
|
+
declare function createTopoloMcpHttpHandler(options?: TopoloMcpHttpOptions): {
|
|
56
|
+
/** Cloudflare Worker / fetch-compatible entrypoint. */
|
|
57
|
+
fetch: (request: Request) => Promise<Response>;
|
|
58
|
+
/** Exposed for tests/introspection. */
|
|
59
|
+
sessions: Map<string, McpSession>;
|
|
60
|
+
};
|
|
61
|
+
type TopoloMcpHttpHandler = ReturnType<typeof createTopoloMcpHttpHandler>;
|
|
62
|
+
|
|
63
|
+
export { type TopoloMcpHttpHandler, type TopoloMcpHttpOptions, createTopoloMcpHttpHandler };
|