@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 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 [serviceId, resource] = splitService(servicePart);
25
+ const [appId, resource] = splitService(servicePart);
26
26
  const candidates = new Set([
27
27
  required,
28
28
  "*",
29
- serviceId ? `${serviceId}.*` : null,
30
- serviceId && resource && actionPart ? `${serviceId}.${resource}:*` : null,
31
- serviceId && resource && actionPart ? `${serviceId}.${resource}.${actionPart}` : null,
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 = displayServiceId(entry);
83
+ const id = displayAppId(entry);
84
84
  return {
85
85
  id,
86
- serviceId: entry.serviceId,
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 service id, slug, or name filter."
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 (service id, slug, name, API URL, launch URL, status). Accepts the service id, slug, or name. Resolved against the live registry so newly-registered apps work without a client rebuild.",
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: service id, slug, or name from topolo_list_applications."
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.serviceId.toLowerCase() === needle || e.slug && e.slug.toLowerCase() === needle || e.name && e.name.toLowerCase() === needle
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.serviceId).sort().filter((value) => typeof value === "string" && value.length > 0);
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: action.destructive || action.requiresConfirmation || action.method !== "GET",
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.serviceId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
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 (action.requiresConfirmation || action.method !== "GET") {
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 displayServiceId(entry) {
429
- for (const value of [entry.slug, entry.name, entry.serviceId]) {
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 serviceIdCandidates(value)) {
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 serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
443
+ return appIdCandidates(entry.slug || entry.appId)[0] || entry.appId;
438
444
  }
439
- function serviceIdCandidates(value) {
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 };