@topolo/mcp 0.1.1 → 0.2.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/index.js +195 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import {
|
|
7
|
-
CallToolRequestSchema,
|
|
8
|
-
ListToolsRequestSchema
|
|
9
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
5
|
|
|
11
6
|
// node_modules/@topolo/sdk/dist/index.js
|
|
12
7
|
function applyAuthHeaders(headers, credential) {
|
|
@@ -63,17 +58,38 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
63
58
|
this.path = path;
|
|
64
59
|
}
|
|
65
60
|
};
|
|
66
|
-
var
|
|
61
|
+
var SERVICES = {
|
|
62
|
+
agent: "https://topolo-agent-worker.topolo.workers.dev",
|
|
67
63
|
auth: "https://auth.topolo.app",
|
|
68
|
-
|
|
64
|
+
bugfix: "https://bugfix.topolo.app",
|
|
65
|
+
bytes: "https://r2-media-manager-worker.topolo.workers.dev",
|
|
66
|
+
chat: "https://chat.topolo.app",
|
|
67
|
+
commerce: "https://topolo-commerce-api.topolo.workers.dev",
|
|
68
|
+
crm: "https://crm-api.topolo.app",
|
|
69
|
+
forecast: "https://forecast-worker-production.topolo.workers.dev",
|
|
70
|
+
learn: "https://topololearn-api.topolo.workers.dev",
|
|
71
|
+
mdm: "https://nodo-mdm-api.topolo.app",
|
|
72
|
+
messaging: "https://messages.topolo.app",
|
|
73
|
+
nexus: "https://topolo-nexus-gateway.topolo.workers.dev",
|
|
74
|
+
one: "https://topoloone-worker.topolo.workers.dev",
|
|
75
|
+
pay: "https://topolo-pay.topolo.workers.dev",
|
|
76
|
+
"quro.api": "https://api.ol0.me",
|
|
77
|
+
"quro.redirect": "https://ol0.me",
|
|
78
|
+
roadmapper: "https://roadmapper-api.topolo.app",
|
|
79
|
+
social_studio: "https://topolo-social-studio-api.topolo.workers.dev",
|
|
80
|
+
socialize: "https://socialize-prod-api.topolo.workers.dev",
|
|
81
|
+
support: "https://support.topolo.app",
|
|
82
|
+
"web.api": "https://web-api.topolo.app",
|
|
83
|
+
"web.runtime": "https://sites.topolo.app"
|
|
69
84
|
};
|
|
70
85
|
var PLATFORM_SERVICE_IDS = {
|
|
71
86
|
crm: "srv_iCwM4jGXcwlj"
|
|
72
87
|
};
|
|
88
|
+
var DEFAULT_SERVICE_URLS = SERVICES;
|
|
73
89
|
function resolveServiceUrl(service, overrides) {
|
|
74
90
|
const override = overrides?.[service];
|
|
75
91
|
if (override) return override;
|
|
76
|
-
const envKey = `TOPOLO_SERVICE_URL_${service.toUpperCase()}`;
|
|
92
|
+
const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
|
|
77
93
|
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
78
94
|
if (envValue) return envValue;
|
|
79
95
|
return DEFAULT_SERVICE_URLS[service];
|
|
@@ -86,6 +102,7 @@ var TopoloClient = class {
|
|
|
86
102
|
requireConfirmForWrites;
|
|
87
103
|
timeoutMs;
|
|
88
104
|
fetchImpl;
|
|
105
|
+
debug;
|
|
89
106
|
constructor(options) {
|
|
90
107
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
91
108
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
@@ -95,6 +112,14 @@ var TopoloClient = class {
|
|
|
95
112
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
96
113
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
97
114
|
this.fetchImpl = options.fetch ?? fetch;
|
|
115
|
+
this.debug = options.debug;
|
|
116
|
+
}
|
|
117
|
+
emit(event) {
|
|
118
|
+
if (!this.debug) return;
|
|
119
|
+
try {
|
|
120
|
+
this.debug(event);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
98
123
|
}
|
|
99
124
|
/**
|
|
100
125
|
* Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
|
|
@@ -121,27 +146,56 @@ var TopoloClient = class {
|
|
|
121
146
|
const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
|
|
122
147
|
if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
|
|
123
148
|
applyAuthHeaders(headers, this.credential);
|
|
124
|
-
|
|
149
|
+
const requestId = generateRequestId();
|
|
150
|
+
applyAuditHeaders(headers, this.agent, requestId);
|
|
125
151
|
if (opts.headers) {
|
|
126
152
|
for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
|
|
127
153
|
}
|
|
128
154
|
const controller = new AbortController();
|
|
129
155
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
130
156
|
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
157
|
+
const urlStr = url.toString();
|
|
158
|
+
const startedAt = Date.now();
|
|
159
|
+
this.emit({ phase: "request", method, service: opts.service, path: opts.path, url: urlStr, requestId });
|
|
131
160
|
let res;
|
|
132
161
|
try {
|
|
133
|
-
res = await this.fetchImpl(
|
|
162
|
+
res = await this.fetchImpl(urlStr, {
|
|
134
163
|
method,
|
|
135
164
|
headers,
|
|
136
165
|
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
137
166
|
signal
|
|
138
167
|
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.emit({
|
|
170
|
+
phase: "error",
|
|
171
|
+
method,
|
|
172
|
+
service: opts.service,
|
|
173
|
+
path: opts.path,
|
|
174
|
+
url: urlStr,
|
|
175
|
+
requestId,
|
|
176
|
+
durationMs: Date.now() - startedAt,
|
|
177
|
+
error: err instanceof Error ? err.message : String(err)
|
|
178
|
+
});
|
|
179
|
+
throw err;
|
|
139
180
|
} finally {
|
|
140
181
|
clearTimeout(timeoutId);
|
|
141
182
|
}
|
|
142
183
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
143
184
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
144
185
|
if (!res.ok) {
|
|
186
|
+
const durationMs = Date.now() - startedAt;
|
|
187
|
+
const message = describeError(parsed, `HTTP ${res.status}`);
|
|
188
|
+
this.emit({
|
|
189
|
+
phase: "error",
|
|
190
|
+
method,
|
|
191
|
+
service: opts.service,
|
|
192
|
+
path: opts.path,
|
|
193
|
+
url: urlStr,
|
|
194
|
+
requestId,
|
|
195
|
+
durationMs,
|
|
196
|
+
error: message,
|
|
197
|
+
status: res.status
|
|
198
|
+
});
|
|
145
199
|
if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
|
|
146
200
|
if (res.status === 403) {
|
|
147
201
|
throw new TopoloPermissionError(
|
|
@@ -151,6 +205,16 @@ var TopoloClient = class {
|
|
|
151
205
|
}
|
|
152
206
|
throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
|
|
153
207
|
}
|
|
208
|
+
this.emit({
|
|
209
|
+
phase: "response",
|
|
210
|
+
method,
|
|
211
|
+
service: opts.service,
|
|
212
|
+
path: opts.path,
|
|
213
|
+
url: urlStr,
|
|
214
|
+
requestId,
|
|
215
|
+
status: res.status,
|
|
216
|
+
durationMs: Date.now() - startedAt
|
|
217
|
+
});
|
|
154
218
|
return parsed;
|
|
155
219
|
}
|
|
156
220
|
/**
|
|
@@ -308,6 +372,13 @@ function resolveServiceUrlsFromEnv() {
|
|
|
308
372
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
309
373
|
}
|
|
310
374
|
|
|
375
|
+
// src/server.ts
|
|
376
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
377
|
+
import {
|
|
378
|
+
CallToolRequestSchema,
|
|
379
|
+
ListToolsRequestSchema
|
|
380
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
381
|
+
|
|
311
382
|
// src/gating.ts
|
|
312
383
|
function hasScope(set, required) {
|
|
313
384
|
if (set.role === "super_admin") return true;
|
|
@@ -340,6 +411,8 @@ function splitService(servicePart) {
|
|
|
340
411
|
}
|
|
341
412
|
|
|
342
413
|
// src/tools.ts
|
|
414
|
+
var SERVICE_IDS = Object.keys(DEFAULT_SERVICE_URLS).sort();
|
|
415
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
343
416
|
var TOOLS = [
|
|
344
417
|
{
|
|
345
418
|
name: "topolo_whoami",
|
|
@@ -377,6 +450,75 @@ var TOOLS = [
|
|
|
377
450
|
});
|
|
378
451
|
}
|
|
379
452
|
},
|
|
453
|
+
{
|
|
454
|
+
name: "topolo_api_call",
|
|
455
|
+
title: "Call any Topolo platform service",
|
|
456
|
+
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(", ")}.`,
|
|
457
|
+
requiredScopes: [],
|
|
458
|
+
destructive: true,
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {
|
|
462
|
+
service: {
|
|
463
|
+
type: "string",
|
|
464
|
+
enum: SERVICE_IDS,
|
|
465
|
+
description: "Target service ID from the platform registry."
|
|
466
|
+
},
|
|
467
|
+
method: {
|
|
468
|
+
type: "string",
|
|
469
|
+
enum: HTTP_METHODS,
|
|
470
|
+
default: "GET",
|
|
471
|
+
description: "HTTP method. Defaults to GET."
|
|
472
|
+
},
|
|
473
|
+
path: {
|
|
474
|
+
type: "string",
|
|
475
|
+
minLength: 1,
|
|
476
|
+
description: 'Request path including any leading slash, e.g. "/api/contacts".'
|
|
477
|
+
},
|
|
478
|
+
body: {
|
|
479
|
+
description: "JSON request body. Ignored for GET."
|
|
480
|
+
},
|
|
481
|
+
query: {
|
|
482
|
+
type: "object",
|
|
483
|
+
additionalProperties: { type: "string" },
|
|
484
|
+
description: "Query string parameters as string values."
|
|
485
|
+
},
|
|
486
|
+
confirm: {
|
|
487
|
+
type: "boolean",
|
|
488
|
+
description: "Required for non-GET methods. Explicit human-in-the-loop acknowledgement for writes."
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
required: ["service", "path"],
|
|
492
|
+
additionalProperties: false
|
|
493
|
+
},
|
|
494
|
+
handler: async (topolo, args) => {
|
|
495
|
+
const service = args["service"];
|
|
496
|
+
const path = args["path"];
|
|
497
|
+
if (typeof service !== "string" || !SERVICE_IDS.includes(service)) {
|
|
498
|
+
throw new Error(`Unknown service "${String(service)}". Known: ${SERVICE_IDS.join(", ")}`);
|
|
499
|
+
}
|
|
500
|
+
if (typeof path !== "string" || !path) {
|
|
501
|
+
throw new Error("path is required");
|
|
502
|
+
}
|
|
503
|
+
const methodRaw = args["method"] ?? "GET";
|
|
504
|
+
if (typeof methodRaw !== "string" || !HTTP_METHODS.includes(methodRaw)) {
|
|
505
|
+
throw new Error(`Unknown HTTP method "${String(methodRaw)}"`);
|
|
506
|
+
}
|
|
507
|
+
const method = methodRaw;
|
|
508
|
+
const query = args["query"];
|
|
509
|
+
const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
|
|
510
|
+
Object.entries(query).map(([k, v]) => [k, String(v)])
|
|
511
|
+
) : void 0;
|
|
512
|
+
return topolo.client.request({
|
|
513
|
+
service,
|
|
514
|
+
method,
|
|
515
|
+
path,
|
|
516
|
+
body: args["body"],
|
|
517
|
+
...normalisedQuery ? { query: normalisedQuery } : {},
|
|
518
|
+
confirm: Boolean(args["confirm"])
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
},
|
|
380
522
|
{
|
|
381
523
|
name: "topolo_crm_get_contact",
|
|
382
524
|
title: "Fetch a single CRM contact",
|
|
@@ -409,37 +551,12 @@ function filterToolsByScopes(tools, scopes) {
|
|
|
409
551
|
// src/version.ts
|
|
410
552
|
var MCP_VERSION = "0.1.0";
|
|
411
553
|
|
|
412
|
-
// src/
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
if (!credential) {
|
|
416
|
-
process.stderr.write(
|
|
417
|
-
"TopoloMCP: TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set in the environment.\n"
|
|
418
|
-
);
|
|
419
|
-
process.exit(1);
|
|
420
|
-
}
|
|
421
|
-
const agentName = process.env.TOPOLO_AGENT_NAME;
|
|
422
|
-
const serviceUrls = resolveServiceUrlsFromEnv();
|
|
423
|
-
const topolo = createTopolo({
|
|
424
|
-
credential,
|
|
425
|
-
agent: {
|
|
426
|
-
clientName: "topolo-mcp",
|
|
427
|
-
clientVersion: MCP_VERSION,
|
|
428
|
-
...agentName ? { agentName } : {}
|
|
429
|
-
},
|
|
430
|
-
serviceUrls,
|
|
431
|
-
requireConfirmForWrites: true
|
|
432
|
-
});
|
|
433
|
-
const scopes = await loadScopes(topolo);
|
|
434
|
-
const availableTools = filterToolsByScopes(TOOLS, scopes);
|
|
554
|
+
// src/server.ts
|
|
555
|
+
function buildServer(topolo, scopes, tools = TOOLS) {
|
|
556
|
+
const availableTools = filterToolsByScopes(tools, scopes);
|
|
435
557
|
const server = new Server(
|
|
436
|
-
{
|
|
437
|
-
|
|
438
|
-
version: MCP_VERSION
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
capabilities: { tools: {} }
|
|
442
|
-
}
|
|
558
|
+
{ name: "topolo", version: MCP_VERSION },
|
|
559
|
+
{ capabilities: { tools: {} } }
|
|
443
560
|
);
|
|
444
561
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
445
562
|
tools: availableTools.map((t) => ({
|
|
@@ -469,9 +586,7 @@ async function main() {
|
|
|
469
586
|
try {
|
|
470
587
|
const result = await tool.handler(topolo, params.arguments ?? {});
|
|
471
588
|
return {
|
|
472
|
-
content: [
|
|
473
|
-
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
474
|
-
]
|
|
589
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
475
590
|
};
|
|
476
591
|
} catch (err) {
|
|
477
592
|
if (err instanceof TopoloPermissionError) {
|
|
@@ -483,10 +598,46 @@ async function main() {
|
|
|
483
598
|
return toolError(err instanceof Error ? err.message : String(err));
|
|
484
599
|
}
|
|
485
600
|
});
|
|
601
|
+
return server;
|
|
602
|
+
}
|
|
603
|
+
function advertisedToolNames(scopes, tools = TOOLS) {
|
|
604
|
+
return filterToolsByScopes(tools, scopes).map((t) => t.name);
|
|
605
|
+
}
|
|
606
|
+
function toolError(message) {
|
|
607
|
+
return {
|
|
608
|
+
isError: true,
|
|
609
|
+
content: [{ type: "text", text: message }]
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/index.ts
|
|
614
|
+
async function main() {
|
|
615
|
+
const credential = resolveCredentialFromEnv();
|
|
616
|
+
if (!credential) {
|
|
617
|
+
process.stderr.write(
|
|
618
|
+
"TopoloMCP: TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set in the environment.\n"
|
|
619
|
+
);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
const agentName = process.env.TOPOLO_AGENT_NAME;
|
|
623
|
+
const serviceUrls = resolveServiceUrlsFromEnv();
|
|
624
|
+
const topolo = createTopolo({
|
|
625
|
+
credential,
|
|
626
|
+
agent: {
|
|
627
|
+
clientName: "topolo-mcp",
|
|
628
|
+
clientVersion: MCP_VERSION,
|
|
629
|
+
...agentName ? { agentName } : {}
|
|
630
|
+
},
|
|
631
|
+
serviceUrls,
|
|
632
|
+
requireConfirmForWrites: true
|
|
633
|
+
});
|
|
634
|
+
const scopes = await loadScopes(topolo);
|
|
635
|
+
const server = buildServer(topolo, scopes);
|
|
486
636
|
const transport = new StdioServerTransport();
|
|
487
637
|
await server.connect(transport);
|
|
638
|
+
const toolNames = advertisedToolNames(scopes);
|
|
488
639
|
process.stderr.write(
|
|
489
|
-
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${
|
|
640
|
+
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
|
|
490
641
|
`
|
|
491
642
|
);
|
|
492
643
|
}
|
|
@@ -506,12 +657,6 @@ async function loadScopes(topolo) {
|
|
|
506
657
|
throw err;
|
|
507
658
|
}
|
|
508
659
|
}
|
|
509
|
-
function toolError(message) {
|
|
510
|
-
return {
|
|
511
|
-
isError: true,
|
|
512
|
-
content: [{ type: "text", text: message }]
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
660
|
main().catch((err) => {
|
|
516
661
|
process.stderr.write(`TopoloMCP fatal: ${err instanceof Error ? err.message : String(err)}
|
|
517
662
|
`);
|
package/package.json
CHANGED