@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.
- package/README.md +79 -9
- package/dist/index.js +101 -48
- 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
|
-
|
|
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
|
-
|
|
83
|
+
Exactly one credential var must be set. If both are present, `TOPOLO_API_KEY`
|
|
84
|
+
wins.
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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/
|
|
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);
|
|
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
|
-
|
|
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: ${
|
|
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.
|
|
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": "
|
|
24
|
+
"@topolo/sdk": "latest",
|
|
25
25
|
"@types/node": "^20.12.0",
|
|
26
26
|
"tsup": "^8.0.0",
|
|
27
27
|
"typescript": "^5.4.0",
|