@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.
Files changed (2) hide show
  1. package/dist/index.js +195 -50
  2. 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 DEFAULT_SERVICE_URLS = {
61
+ var SERVICES = {
62
+ agent: "https://topolo-agent-worker.topolo.workers.dev",
67
63
  auth: "https://auth.topolo.app",
68
- crm: "https://topolo-crm-worker.topolo.workers.dev"
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
- applyAuditHeaders(headers, this.agent, generateRequestId());
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(url.toString(), {
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/index.ts
413
- async function main() {
414
- const credential = resolveCredentialFromEnv();
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
- name: "topolo",
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: ${availableTools.map((t) => t.name).join(", ") || "(none)"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol server for the Topolo platform. Exposes scope-gated tools that third-party agents (Claude, Codex, etc.) can call natively.",
5
5
  "type": "module",
6
6
  "bin": {