@topolo/mcp 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +79 -9
  2. package/dist/index.js +101 -48
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -6,8 +6,8 @@ tools rather than shelling out.
6
6
 
7
7
  ## Why both a CLI and an MCP server?
8
8
 
9
- - The **CLI** is for humans, shell scripts, and agents without MCP support.
10
- - The **MCP server** is for agents that speak the protocol natively — it gives
9
+ - The **CLI** (`@topolo/cli`) is for humans, shell scripts, and agents without MCP support.
10
+ - The **MCP server** (`@topolo/mcp`) is for agents that speak the protocol natively — it gives
11
11
  them typed tool schemas, scope-filtered tool advertisement, and structured
12
12
  error responses. Both wrap the same `@topolo/sdk`.
13
13
 
@@ -17,9 +17,37 @@ tools rather than shelling out.
17
17
  npm install -g @topolo/mcp
18
18
  ```
19
19
 
20
+ You don't have to install globally — the registration snippets below use `npx`
21
+ so the server downloads on demand.
22
+
23
+ ## Get a credential
24
+
25
+ Either:
26
+
27
+ - **Long-lived**: mint an API key at the Topolo Developers console
28
+ (`TOPOLO_API_KEY=topo_live_...`). Preferred for persistent agent installs.
29
+ - **Short-lived**: `topolo auth login` with the CLI, then copy the access token
30
+ from `~/.config/topolo/config.json`. Useful for dev and testing.
31
+
20
32
  ## Register with an MCP client
21
33
 
22
- Example (Claude Desktop, `claude_desktop_config.json`):
34
+ ### Claude Code
35
+
36
+ ```bash
37
+ claude mcp add topolo -- npx -y @topolo/mcp
38
+ ```
39
+
40
+ Then set the credential and (optional) agent label in your shell profile so
41
+ Claude Code inherits them when it spawns the server:
42
+
43
+ ```bash
44
+ export TOPOLO_API_KEY=topo_live_...
45
+ export TOPOLO_AGENT_NAME=claude-code
46
+ ```
47
+
48
+ ### Claude Desktop
49
+
50
+ `claude_desktop_config.json`:
23
51
 
24
52
  ```json
25
53
  {
@@ -36,6 +64,13 @@ Example (Claude Desktop, `claude_desktop_config.json`):
36
64
  }
37
65
  ```
38
66
 
67
+ ### Codex / Cursor / generic MCP host
68
+
69
+ Any MCP host that spawns a stdio subprocess works. Point `command` at
70
+ `npx -y @topolo/mcp` and pass the same env vars. Most Codex-style setups also
71
+ read `AGENTS.md` files — see `@topolo/cli`'s `skills/codex/AGENTS.md` for a
72
+ ready-made agent guide that covers both the CLI and this MCP.
73
+
39
74
  ## Supported env vars
40
75
 
41
76
  | Var | Purpose |
@@ -43,13 +78,21 @@ Example (Claude Desktop, `claude_desktop_config.json`):
43
78
  | `TOPOLO_API_KEY` | Platform API key (preferred) |
44
79
  | `TOPOLO_ACCESS_TOKEN` | Short-lived JWT (dev/testing) |
45
80
  | `TOPOLO_AGENT_NAME` | Human-readable agent label for audit logs |
46
- | `TOPOLO_SERVICE_URL_<ID>` | Override a service base URL (`_AUTH`, `_CRM`, ...) |
81
+ | `TOPOLO_SERVICE_URL_<ID>` | Override a service base URL (e.g. `_AUTH`, `_CRM`) |
47
82
 
48
- On startup the server:
83
+ Exactly one credential var must be set. If both are present, `TOPOLO_API_KEY`
84
+ wins.
49
85
 
50
- 1. Refuses to start if no credential is set.
51
- 2. Introspects the credential against TopoloAuth to load granted scopes.
52
- 3. Filters the advertised tool list to only those the credential can use.
86
+ ## Startup sequence
87
+
88
+ 1. Resolve the credential from env. **Refuse to start** if none is set.
89
+ 2. Call `GET /api/me` to load the user's granted scopes + role.
90
+ 3. Filter `TOOLS` by those scopes — agents never see a tool they cannot use.
91
+ 4. Connect the stdio transport and begin serving MCP requests.
92
+
93
+ Startup is synchronous and fast (< 500 ms typical). If the credential is
94
+ rejected, the process logs the error to stderr and exits non-zero — host
95
+ clients surface that as a failed server launch.
53
96
 
54
97
  ## Tools (Phase 1)
55
98
 
@@ -76,6 +119,22 @@ later phases alongside typed SDK modules, each annotated with
76
119
  the call explicitly passes `confirm: true`. Mutating tools (when introduced)
77
120
  will require the host client to approve before invocation.
78
121
 
122
+ ## Troubleshooting
123
+
124
+ - **"TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set"** — no credential
125
+ reached the subprocess. In Claude Desktop, double-check the `env` block in
126
+ `claude_desktop_config.json`. In Claude Code, the server inherits your shell
127
+ env, so make sure the var is exported in the profile your launcher reads.
128
+ - **"credential rejected"** — token is expired or revoked. For API keys, mint
129
+ a fresh one. For access tokens, re-run `topolo auth login` and copy the new
130
+ token.
131
+ - **Tool missing from the list** — the credential doesn't grant the required
132
+ scope. Check `topolo whoami` to see the current scopes; either broaden the
133
+ API key's permissions or switch to one that already has them.
134
+ - **Tool call returns `Permission denied`** — the scope check on the backend
135
+ disagrees with startup introspection (rare, but possible across scope
136
+ changes). Re-spawn the server to refresh the cached scope set.
137
+
79
138
  ## Development
80
139
 
81
140
  ```bash
@@ -86,7 +145,17 @@ TOPOLO_API_KEY=topo_live_... node dist/index.js
86
145
  ```
87
146
 
88
147
  The server speaks MCP over stdio; when running standalone it just blocks
89
- waiting for JSON-RPC on stdin.
148
+ waiting for JSON-RPC on stdin. To drive it manually, use the MCP Inspector:
149
+
150
+ ```bash
151
+ npx @modelcontextprotocol/inspector node dist/index.js
152
+ ```
153
+
154
+ ## Release flow
155
+
156
+ Pushing a `v<x.y.z>` tag to `main` triggers GitHub Actions to typecheck, test,
157
+ build, verify the tag matches `package.json`, and publish to npm via the
158
+ `NPM_TOKEN` secret. No developer-machine credentials involved.
90
159
 
91
160
  ## Phase 2 (planned)
92
161
 
@@ -94,3 +163,4 @@ waiting for JSON-RPC on stdin.
94
163
  in TopoloAuth.
95
164
  - Dynamic tool registration as more `@topolo/sdk` modules ship.
96
165
  - HTTP transport in addition to stdio, for hosted (non-subprocess) deployments.
166
+ - Optional `--provenance` on npm publish once the repo is public.
package/dist/index.js CHANGED
@@ -1,14 +1,9 @@
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
- // ../packages/topolo-sdk/dist/index.js
6
+ // node_modules/@topolo/sdk/dist/index.js
12
7
  function applyAuthHeaders(headers, credential) {
13
8
  if (credential.kind === "api_key") {
14
9
  headers.set("X-Api-Key", credential.apiKey);
@@ -86,6 +81,7 @@ var TopoloClient = class {
86
81
  requireConfirmForWrites;
87
82
  timeoutMs;
88
83
  fetchImpl;
84
+ debug;
89
85
  constructor(options) {
90
86
  if (!options.credential) throw new TopoloAuthError("credential is required");
91
87
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
@@ -95,6 +91,14 @@ var TopoloClient = class {
95
91
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
96
92
  this.timeoutMs = options.timeoutMs ?? 3e4;
97
93
  this.fetchImpl = options.fetch ?? fetch;
94
+ this.debug = options.debug;
95
+ }
96
+ emit(event) {
97
+ if (!this.debug) return;
98
+ try {
99
+ this.debug(event);
100
+ } catch {
101
+ }
98
102
  }
99
103
  /**
100
104
  * Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
@@ -121,27 +125,56 @@ var TopoloClient = class {
121
125
  const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
122
126
  if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
123
127
  applyAuthHeaders(headers, this.credential);
124
- applyAuditHeaders(headers, this.agent, generateRequestId());
128
+ const requestId = generateRequestId();
129
+ applyAuditHeaders(headers, this.agent, requestId);
125
130
  if (opts.headers) {
126
131
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
127
132
  }
128
133
  const controller = new AbortController();
129
134
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
130
135
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
136
+ const urlStr = url.toString();
137
+ const startedAt = Date.now();
138
+ this.emit({ phase: "request", method, service: opts.service, path: opts.path, url: urlStr, requestId });
131
139
  let res;
132
140
  try {
133
- res = await this.fetchImpl(url.toString(), {
141
+ res = await this.fetchImpl(urlStr, {
134
142
  method,
135
143
  headers,
136
144
  body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
137
145
  signal
138
146
  });
147
+ } catch (err) {
148
+ this.emit({
149
+ phase: "error",
150
+ method,
151
+ service: opts.service,
152
+ path: opts.path,
153
+ url: urlStr,
154
+ requestId,
155
+ durationMs: Date.now() - startedAt,
156
+ error: err instanceof Error ? err.message : String(err)
157
+ });
158
+ throw err;
139
159
  } finally {
140
160
  clearTimeout(timeoutId);
141
161
  }
142
162
  const contentType = res.headers.get("Content-Type") ?? "";
143
163
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
144
164
  if (!res.ok) {
165
+ const durationMs = Date.now() - startedAt;
166
+ const message = describeError(parsed, `HTTP ${res.status}`);
167
+ this.emit({
168
+ phase: "error",
169
+ method,
170
+ service: opts.service,
171
+ path: opts.path,
172
+ url: urlStr,
173
+ requestId,
174
+ durationMs,
175
+ error: message,
176
+ status: res.status
177
+ });
145
178
  if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
146
179
  if (res.status === 403) {
147
180
  throw new TopoloPermissionError(
@@ -151,6 +184,16 @@ var TopoloClient = class {
151
184
  }
152
185
  throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
153
186
  }
187
+ this.emit({
188
+ phase: "response",
189
+ method,
190
+ service: opts.service,
191
+ path: opts.path,
192
+ url: urlStr,
193
+ requestId,
194
+ status: res.status,
195
+ durationMs: Date.now() - startedAt
196
+ });
154
197
  return parsed;
155
198
  }
156
199
  /**
@@ -308,6 +351,13 @@ function resolveServiceUrlsFromEnv() {
308
351
  return Object.keys(out).length > 0 ? out : void 0;
309
352
  }
310
353
 
354
+ // src/server.ts
355
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
356
+ import {
357
+ CallToolRequestSchema,
358
+ ListToolsRequestSchema
359
+ } from "@modelcontextprotocol/sdk/types.js";
360
+
311
361
  // src/gating.ts
312
362
  function hasScope(set, required) {
313
363
  if (set.role === "super_admin") return true;
@@ -409,37 +459,12 @@ function filterToolsByScopes(tools, scopes) {
409
459
  // src/version.ts
410
460
  var MCP_VERSION = "0.1.0";
411
461
 
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);
462
+ // src/server.ts
463
+ function buildServer(topolo, scopes, tools = TOOLS) {
464
+ const availableTools = filterToolsByScopes(tools, scopes);
435
465
  const server = new Server(
436
- {
437
- name: "topolo",
438
- version: MCP_VERSION
439
- },
440
- {
441
- capabilities: { tools: {} }
442
- }
466
+ { name: "topolo", version: MCP_VERSION },
467
+ { capabilities: { tools: {} } }
443
468
  );
444
469
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
445
470
  tools: availableTools.map((t) => ({
@@ -469,9 +494,7 @@ async function main() {
469
494
  try {
470
495
  const result = await tool.handler(topolo, params.arguments ?? {});
471
496
  return {
472
- content: [
473
- { type: "text", text: JSON.stringify(result, null, 2) }
474
- ]
497
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
475
498
  };
476
499
  } catch (err) {
477
500
  if (err instanceof TopoloPermissionError) {
@@ -483,10 +506,46 @@ async function main() {
483
506
  return toolError(err instanceof Error ? err.message : String(err));
484
507
  }
485
508
  });
509
+ return server;
510
+ }
511
+ function advertisedToolNames(scopes, tools = TOOLS) {
512
+ return filterToolsByScopes(tools, scopes).map((t) => t.name);
513
+ }
514
+ function toolError(message) {
515
+ return {
516
+ isError: true,
517
+ content: [{ type: "text", text: message }]
518
+ };
519
+ }
520
+
521
+ // src/index.ts
522
+ async function main() {
523
+ const credential = resolveCredentialFromEnv();
524
+ if (!credential) {
525
+ process.stderr.write(
526
+ "TopoloMCP: TOPOLO_API_KEY or TOPOLO_ACCESS_TOKEN must be set in the environment.\n"
527
+ );
528
+ process.exit(1);
529
+ }
530
+ const agentName = process.env.TOPOLO_AGENT_NAME;
531
+ const serviceUrls = resolveServiceUrlsFromEnv();
532
+ const topolo = createTopolo({
533
+ credential,
534
+ agent: {
535
+ clientName: "topolo-mcp",
536
+ clientVersion: MCP_VERSION,
537
+ ...agentName ? { agentName } : {}
538
+ },
539
+ serviceUrls,
540
+ requireConfirmForWrites: true
541
+ });
542
+ const scopes = await loadScopes(topolo);
543
+ const server = buildServer(topolo, scopes);
486
544
  const transport = new StdioServerTransport();
487
545
  await server.connect(transport);
546
+ const toolNames = advertisedToolNames(scopes);
488
547
  process.stderr.write(
489
- `TopoloMCP v${MCP_VERSION} connected. Tools available: ${availableTools.map((t) => t.name).join(", ") || "(none)"}
548
+ `TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
490
549
  `
491
550
  );
492
551
  }
@@ -506,12 +565,6 @@ async function loadScopes(topolo) {
506
565
  throw err;
507
566
  }
508
567
  }
509
- function toolError(message) {
510
- return {
511
- isError: true,
512
- content: [{ type: "text", text: message }]
513
- };
514
- }
515
568
  main().catch((err) => {
516
569
  process.stderr.write(`TopoloMCP fatal: ${err instanceof Error ? err.message : String(err)}
517
570
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": {
@@ -21,7 +21,7 @@
21
21
  "@modelcontextprotocol/sdk": "^1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@topolo/sdk": "file:../packages/topolo-sdk",
24
+ "@topolo/sdk": "latest",
25
25
  "@types/node": "^20.12.0",
26
26
  "tsup": "^8.0.0",
27
27
  "typescript": "^5.4.0",