@topolo/mcp 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -112,7 +112,11 @@ clients surface that as a failed server launch.
112
112
  `topolo_api_call` is the escape hatch for endpoints without typed MCP tools
113
113
  yet. GET requests work without confirmation; mutating HTTP methods must pass
114
114
  `confirm: true` or the SDK rejects the call. Backing services still enforce
115
- permissions for every request.
115
+ permissions for every request. The tool schema intentionally does not embed a
116
+ large service enum; agents should call `topolo_list_services` first, then pass a
117
+ service ID/slug from that credential-scoped result. New services become callable
118
+ without an MCP release once Auth grants the organization and credential the
119
+ relevant permissions.
116
120
 
117
121
  ## Safety rails
118
122
 
@@ -121,6 +125,9 @@ permissions for every request.
121
125
  to see Org B's data.
122
126
  - **Scope-gated tool advertisement.** Agents never see tools they can't use —
123
127
  fewer wasted attempts and less prompt noise.
128
+ - **Credential-scoped catalog discovery.** `topolo_list_services` and
129
+ `topolo_list_applications` use TopoloAuth's live organization/permission
130
+ catalog. They do not expose the global generated platform catalog.
124
131
  - **Audit headers.** Every request sends `X-Topolo-Client: topolo-mcp/<ver>`,
125
132
  `X-Topolo-Agent: <label>`, `X-Topolo-Request-Id: <uuid>`.
126
133
  - **Write-action confirmation.** The SDK refuses mutating HTTP methods unless
@@ -159,11 +166,12 @@ waiting for JSON-RPC on stdin. To drive it manually, use the MCP Inspector:
159
166
  npx @modelcontextprotocol/inspector node dist/index.js
160
167
  ```
161
168
 
162
- ## Release flow
169
+ ## Release Flow
163
170
 
164
- Pushing a `v<x.y.z>` tag to `main` triggers GitHub Actions to typecheck, test,
165
- build, verify the tag matches `package.json`, and publish to npm via the
166
- `NPM_TOKEN` secret. No developer-machine credentials involved.
171
+ Preferred flow: push a `v<x.y.z>` tag to `main`; GitHub Actions typechecks,
172
+ tests, builds, verifies the tag matches `package.json`, and publishes to npm via
173
+ the `NPM_TOKEN` secret. Operators may publish directly with an npm token only
174
+ after running the same package checks and `npm pack --dry-run`.
167
175
 
168
176
  ## Phase 2 (planned)
169
177
 
package/dist/dispatch.js CHANGED
@@ -52,7 +52,6 @@ function splitService(servicePart) {
52
52
 
53
53
  // src/tools.ts
54
54
  var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
55
- var SERVICE_IDS = Object.keys(DEFAULT_SERVICE_URLS).sort();
56
55
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
57
56
  var TOOLS = [
58
57
  {
@@ -71,7 +70,7 @@ var TOOLS = [
71
70
  {
72
71
  name: "topolo_list_services",
73
72
  title: "List every Topolo service the agent can reach",
74
- description: "Returns the service registry the SDK knows about: one entry per platform service with its stable service ID and current base URL. Use this to discover what `topolo_api_call` can address \u2014 the service IDs returned here are the exact values accepted by that tool. Pure metadata: no credential introspection, no permission check, no external call.",
73
+ description: "Returns the credential-scoped service registry: one entry per platform service the current credential can use in its organization, with granted permissions and base URL. Use this to discover what `topolo_api_call` can address. This is not the global platform catalog.",
75
74
  requiredScopes: [],
76
75
  destructive: false,
77
76
  inputSchema: {
@@ -79,12 +78,21 @@ var TOOLS = [
79
78
  properties: {},
80
79
  additionalProperties: false
81
80
  },
82
- handler: async () => {
83
- const services = Object.keys(DEFAULT_SERVICE_URLS).sort().map((id) => ({
84
- id,
85
- defaultUrl: DEFAULT_SERVICE_URLS[id],
86
- effectiveUrl: resolveServiceUrl(id)
87
- }));
81
+ handler: async (topolo) => {
82
+ const services = (await topolo.client.listServices()).map((entry) => {
83
+ const id = displayServiceId(entry);
84
+ return {
85
+ id,
86
+ serviceId: entry.serviceId,
87
+ slug: entry.slug,
88
+ name: entry.name,
89
+ baseUrl: entry.baseUrl,
90
+ effectiveUrl: resolveServiceUrl(id) || entry.baseUrl,
91
+ surfaceKind: entry.surfaceKind,
92
+ launchable: entry.launchable,
93
+ permissions: [...entry.permissions]
94
+ };
95
+ }).sort((left, right) => left.id.localeCompare(right.id));
88
96
  return { services };
89
97
  }
90
98
  },
@@ -131,9 +139,9 @@ var TOOLS = [
131
139
  (e) => e.serviceId.toLowerCase() === needle || e.slug && e.slug.toLowerCase() === needle || e.name && e.name.toLowerCase() === needle
132
140
  );
133
141
  if (!match) {
134
- const known = catalog.map((e) => e.slug || e.serviceId).sort().join(", ");
142
+ const known = catalog.map((e) => e.slug || e.serviceId).sort().filter((value) => typeof value === "string" && value.length > 0);
135
143
  throw new Error(
136
- `Unknown application "${application}". Known: ${known}`
144
+ `Unknown application "${application}". ${formatKnownValues(known, "applications")}`
137
145
  );
138
146
  }
139
147
  return { application: match };
@@ -150,7 +158,6 @@ var TOOLS = [
150
158
  properties: {
151
159
  application: {
152
160
  type: "string",
153
- enum: APPLICATION_IDS,
154
161
  description: "Optional application ID from topolo_list_applications."
155
162
  }
156
163
  },
@@ -168,7 +175,7 @@ var TOOLS = [
168
175
  }
169
176
  if (typeof application !== "string" || !APPLICATION_IDS.includes(application)) {
170
177
  throw new Error(
171
- `Unknown application "${String(application)}". Known: ${APPLICATION_IDS.join(", ")}`
178
+ `Unknown application "${String(application)}". ${formatKnownValues(APPLICATION_IDS, "applications")}`
172
179
  );
173
180
  }
174
181
  const app = APPLICATIONS[application];
@@ -191,7 +198,6 @@ var TOOLS = [
191
198
  properties: {
192
199
  application: {
193
200
  type: "string",
194
- enum: APPLICATION_IDS,
195
201
  description: "Optional application ID from topolo_list_applications."
196
202
  },
197
203
  failOn: {
@@ -216,7 +222,7 @@ var TOOLS = [
216
222
  }
217
223
  if (typeof application !== "string" || !APPLICATION_IDS.includes(application)) {
218
224
  throw new Error(
219
- `Unknown application "${String(application)}". Known: ${APPLICATION_IDS.join(", ")}`
225
+ `Unknown application "${String(application)}". ${formatKnownValues(APPLICATION_IDS, "applications")}`
220
226
  );
221
227
  }
222
228
  report = auditAllApplicationRequirements([application]);
@@ -249,7 +255,7 @@ var TOOLS = [
249
255
  {
250
256
  name: "topolo_api_call",
251
257
  title: "Call any Topolo platform service",
252
- description: `Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Known services: ${SERVICE_IDS.join(", ")}.`,
258
+ description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable service IDs; the SDK can also resolve live Auth-catalog service ids/slugs for newly registered API surfaces.",
253
259
  requiredScopes: [],
254
260
  destructive: true,
255
261
  inputSchema: {
@@ -257,7 +263,6 @@ var TOOLS = [
257
263
  properties: {
258
264
  service: {
259
265
  type: "string",
260
- enum: SERVICE_IDS,
261
266
  description: "Target service ID from the platform registry."
262
267
  },
263
268
  method: {
@@ -290,8 +295,8 @@ var TOOLS = [
290
295
  handler: async (topolo, args) => {
291
296
  const service = args["service"];
292
297
  const path = args["path"];
293
- if (typeof service !== "string" || !SERVICE_IDS.includes(service)) {
294
- throw new Error(`Unknown service "${String(service)}". Known: ${SERVICE_IDS.join(", ")}`);
298
+ if (typeof service !== "string" || !service.trim()) {
299
+ throw new Error("service is required. Call topolo_list_services first to discover callable services.");
295
300
  }
296
301
  if (typeof path !== "string" || !path) {
297
302
  throw new Error("path is required");
@@ -306,7 +311,7 @@ var TOOLS = [
306
311
  Object.entries(query).map(([k, v]) => [k, String(v)])
307
312
  ) : void 0;
308
313
  return topolo.client.request({
309
- service,
314
+ service: service.trim(),
310
315
  method,
311
316
  path,
312
317
  body: args["body"],
@@ -379,6 +384,33 @@ function statusesAtOrWorse(threshold) {
379
384
  if (threshold === "needs_review") return ["missing", "needs_review"];
380
385
  return ["missing", "needs_review", "partial"];
381
386
  }
387
+ function displayServiceId(entry) {
388
+ for (const value of [entry.slug, entry.name, entry.serviceId]) {
389
+ if (typeof value !== "string" || !value.trim()) continue;
390
+ for (const candidate of serviceIdCandidates(value)) {
391
+ if (Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, candidate)) {
392
+ return candidate;
393
+ }
394
+ }
395
+ }
396
+ return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
397
+ }
398
+ function serviceIdCandidates(value) {
399
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
400
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
401
+ return [.../* @__PURE__ */ new Set([
402
+ normalized,
403
+ normalized.replace(/-/g, "_"),
404
+ withoutTopolo,
405
+ withoutTopolo.replace(/-/g, "_")
406
+ ])].filter(Boolean);
407
+ }
408
+ function formatKnownValues(values, label) {
409
+ const sorted = [...values].sort();
410
+ const preview = sorted.slice(0, 25).join(", ");
411
+ const suffix = sorted.length > 25 ? `, ... +${sorted.length - 25} more` : "";
412
+ return `Run discovery for the full ${label} list. Known snapshot sample: ${preview}${suffix}`;
413
+ }
382
414
 
383
415
  // src/dispatch.ts
384
416
  function listAvailableTopoloTools(scopes, tools = TOOLS) {