@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 +13 -5
- package/dist/dispatch.js +51 -19
- package/dist/index.js +534 -241
- package/package.json +1 -1
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
|
|
169
|
+
## Release Flow
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`NPM_TOKEN` secret.
|
|
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
|
|
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 =
|
|
84
|
-
id
|
|
85
|
-
|
|
86
|
-
|
|
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().
|
|
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}".
|
|
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)}".
|
|
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)}".
|
|
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:
|
|
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" || !
|
|
294
|
-
throw new Error(
|
|
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) {
|