@magnetoagents/mcp 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/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +10 -0
- package/dist/stdio.d.ts +20 -0
- package/dist/stdio.js +209 -0
- package/dist/tool-catalog.d.ts +23 -0
- package/dist/tool-catalog.js +431 -0
- package/dist/tool-http.d.ts +12 -0
- package/dist/tool-http.js +282 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Magneto contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# `@magnetoagents/mcp`
|
|
2
|
+
|
|
3
|
+
Stdio MCP server for Magneto computers. Implements the same tools as the hosted remote endpoint at `/mcp` against the public Worker API (`/api/v1` + `sk_live_*`).
|
|
4
|
+
|
|
5
|
+
The installed command is **`magneto-mcp`**. Protocol is `2025-03-26`. `serverInfo.version` is the `package.json` `version` field (not a hand-synced literal).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
| Method | Command |
|
|
10
|
+
|--------|---------|
|
|
11
|
+
| npm (global) | `npm install -g @magnetoagents/mcp` |
|
|
12
|
+
| npx | `npx @magnetoagents/mcp` |
|
|
13
|
+
| from-source | `cd packages/mcp && npm ci && npm run build` then `node dist/stdio.js` / `npm link` |
|
|
14
|
+
|
|
15
|
+
The first registry publish waits on the `@magneto` npm org + `NPM_TOKEN` repo secret. Until then, use from-source.
|
|
16
|
+
|
|
17
|
+
## Quickstart (Claude Code, stdio)
|
|
18
|
+
|
|
19
|
+
About 90 seconds after you have an `sk_live_*` key (mint in the dashboard):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export MAGNETO_API_KEY=sk_live_…
|
|
23
|
+
# optional: MAGNETO_API_BASE=http://localhost:${FRONTEND_PORT:-3000}
|
|
24
|
+
claude mcp add magneto --env MAGNETO_API_KEY="$MAGNETO_API_KEY" -- magneto-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
From-source, after `npm ci && npm run build` in `packages/mcp`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude mcp add magneto --env MAGNETO_API_KEY=sk_live_… -- \
|
|
31
|
+
node /absolute/path/to/magneto-app/packages/mcp/dist/stdio.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Versioning (v0.x)
|
|
35
|
+
|
|
36
|
+
Bump this package's `package.json` `version` by hand (patch = fix, minor = additive or breaking while major is 0). That field is the single source for `serverInfo.version`. Publish is `workflow_dispatch` on `.github/workflows/npm-publish.yml`, not a git tag.
|
|
37
|
+
|
|
38
|
+
Bumping this package's version also requires bumping the hosted `MCP_SERVER_VERSION` literal in `app/client/src/lib/mcp/tool-catalog.ts` so `registry-parity.test.ts` stays green. Hosted MCP is not a published package; the duplicate-with-test catalog is the lockstep.
|
|
39
|
+
|
|
40
|
+
## Environment
|
|
41
|
+
|
|
42
|
+
| Variable | Required | Description |
|
|
43
|
+
|----------|----------|-------------|
|
|
44
|
+
| `MAGNETO_API_KEY` | yes* | Workspace key (`sk_live_…`) |
|
|
45
|
+
| `MAGNETO_API_BASE` | no | Origin or `…/api/v1` (default `https://magnetoapp.io`) |
|
|
46
|
+
|
|
47
|
+
\*Falls back to `~/.magneto/credentials.json` from `magneto login` if env is unset. Missing key does not fail `initialize` / `ping`.
|
|
48
|
+
|
|
49
|
+
## Scope filter (account-first, degrade to full list)
|
|
50
|
+
|
|
51
|
+
On the first `tools/list`, stdio calls `GET /account` and caches `key.scopes` for the process.
|
|
52
|
+
|
|
53
|
+
- If that succeeds, the listed tools are filtered to those whose `requiredScope` is in the cached array. A key without `exec` never sees `bash`.
|
|
54
|
+
- If it fails (401, `Missing scope: account`, network), stdio logs a one-line stderr warning and returns the **full catalog**. `tools/call` then surfaces HTTP `{ detail }` as `isError: true`.
|
|
55
|
+
- A forced `tools/call` of a tool hidden by the cached scopes returns the same 403 tool-error (`{ "detail": "Missing scope: …", "status": 403 }`) **without** fetching.
|
|
56
|
+
|
|
57
|
+
Hosted `/mcp` always filters from `requireApiKey` scopes. Stdio may disagree with hosted `tools/list` only when `GET /account` fails.
|
|
58
|
+
|
|
59
|
+
## Claude Code (stdio)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude mcp add magneto --env MAGNETO_API_KEY=sk_live_… -- \
|
|
63
|
+
node /absolute/path/to/magneto-app/packages/mcp/dist/stdio.js
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or with a local base:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add magneto \
|
|
70
|
+
--env MAGNETO_API_KEY=sk_live_… \
|
|
71
|
+
--env MAGNETO_API_BASE=http://localhost:3000 -- \
|
|
72
|
+
node ./packages/mcp/dist/stdio.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Hosted remote MCP (preferred for remote clients)
|
|
76
|
+
|
|
77
|
+
Production: `https://magnetoapp.io/mcp`
|
|
78
|
+
Local: `http://localhost:${FRONTEND_PORT:-3000}/mcp` (rewrites to `/api/mcp`)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
claude mcp add --transport http magneto https://magnetoapp.io/mcp
|
|
82
|
+
# Configure Authorization: Bearer sk_live_… per client version
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tools
|
|
86
|
+
|
|
87
|
+
Duplicate-with-test catalog: `src/tool-catalog.ts` must match `app/client/src/lib/mcp/tool-catalog.ts` (parity test in the client package).
|
|
88
|
+
|
|
89
|
+
| Tool | Scope | Notes |
|
|
90
|
+
|------|--------|--------|
|
|
91
|
+
| `create` | computers | Force workspace from key; client `workspace_id` is omitted |
|
|
92
|
+
| `list` | computers | Minting user scope |
|
|
93
|
+
| `get` | computers | Computer detail |
|
|
94
|
+
| `start` / `stop` / `restart` | computers | Lifecycle |
|
|
95
|
+
| `delete` | computers | **DESTRUCTIVE.** `confirm` must equal the computer name |
|
|
96
|
+
| `resize` | computers | Desktop grow-only; agent/shrink → 422 |
|
|
97
|
+
| `uptime` | computers | Optional `window_seconds` |
|
|
98
|
+
| `gateway_url` | computers | 60-s URL. Desktop `/desk`, agent `/connect`. Do not log `url` |
|
|
99
|
+
| `screenshot` | computers | Desktop → action API; agent → structured status |
|
|
100
|
+
| `action` | computers | One tool, ten GUI actions; agent → structured status |
|
|
101
|
+
| `bash` | **exec** | Opt-in (not in `DEFAULT_SCOPES`). Desktop and agent |
|
|
102
|
+
| `run` | runs | Desktop CU only. Burns CU cap / BYO. Concurrent-run ceiling |
|
|
103
|
+
| `stop_run` | runs | `{ run_id }` |
|
|
104
|
+
| `list_runs` / `get_run` | runs | History; do not log signed screenshot URLs |
|
|
105
|
+
| `install_skill` / `uninstall_skill` | computers | Marketplace skill on a computer |
|
|
106
|
+
| `list_skills` | catalog | `GET /skills` (not installed-on-computer) |
|
|
107
|
+
| `search_marketplace` | catalog | Tool-side filter; REST has no `?q=` |
|
|
108
|
+
| `list_templates` | catalog | Marketplace templates |
|
|
109
|
+
| `list_files` | files | Workspace files |
|
|
110
|
+
| `account` | account | Self-budget snapshot |
|
|
111
|
+
| `chat` | runs | Non-stream `POST /chat/completions`. Desktop only. Same CU cap |
|
|
112
|
+
|
|
113
|
+
`tools/call` of a hidden or unauthorized tool is a JSON-RPC **result** with `isError: true` (not HTTP 403). Revoking the key fails subsequent HTTP calls with `403` `{ "detail": "API key revoked" }`.
|
|
114
|
+
|
|
115
|
+
Zero runtime dependencies. Catalog layout is **duplicate-with-test** (not a workspace package).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
/** Read this package's version from package.json (works from src/, dist/, and an installed tarball). */
|
|
3
|
+
export function readPackageVersion() {
|
|
4
|
+
const raw = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
5
|
+
const parsed = JSON.parse(raw);
|
|
6
|
+
if (typeof parsed.version !== 'string' || parsed.version.length === 0) {
|
|
7
|
+
throw new Error('package.json is missing a version');
|
|
8
|
+
}
|
|
9
|
+
return parsed.version;
|
|
10
|
+
}
|
package/dist/stdio.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export declare function api(method: string, pathSuffix: string, body?: unknown): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
status: number;
|
|
5
|
+
data: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function callTool(name: string, args: Record<string, unknown>): Promise<{
|
|
8
|
+
content: Array<{
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
}>;
|
|
12
|
+
isError?: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function handleRpc(msg: {
|
|
15
|
+
jsonrpc?: string;
|
|
16
|
+
id?: string | number | null;
|
|
17
|
+
method?: string;
|
|
18
|
+
params?: Record<string, unknown>;
|
|
19
|
+
}): Promise<unknown | null>;
|
|
20
|
+
export declare function resetStdioState(): void;
|
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal MCP stdio bridge for Magneto (#110 / #439).
|
|
4
|
+
*
|
|
5
|
+
* Speaks JSON-RPC MCP over stdin/stdout and implements tools against the
|
|
6
|
+
* public Worker `/api/v1` surface (same as CLI / remote MCP).
|
|
7
|
+
*
|
|
8
|
+
* Env:
|
|
9
|
+
* MAGNETO_API_KEY — required (sk_live_*)
|
|
10
|
+
* MAGNETO_API_BASE — origin or …/api/v1 (default https://magnetoapp.io)
|
|
11
|
+
*
|
|
12
|
+
* Scope filter: first tools/list calls GET /account and caches key.scopes.
|
|
13
|
+
* If that fails (401, missing account scope, network), degrade to the full
|
|
14
|
+
* catalog and let tools/call surface HTTP { detail } as isError.
|
|
15
|
+
*
|
|
16
|
+
* Publish is workflow_dispatch on npm-publish.yml (inert until NPM_TOKEN).
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import readline from 'node:readline';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
23
|
+
import { findToolDef, listToolsForScopes, missingScopePayload, MCP_PROTOCOL_VERSION, MCP_SERVER_VERSION, MCP_TOOL_CATALOG, } from './tool-catalog.js';
|
|
24
|
+
import { callToolHttp } from './tool-http.js';
|
|
25
|
+
function normalizeApiBase(input) {
|
|
26
|
+
const fallback = 'https://magnetoapp.io/api/v1';
|
|
27
|
+
const raw = (input ?? process.env.MAGNETO_API_BASE ?? process.env.MAGNETO_BASE_URL ?? fallback)
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/\/+$/, '');
|
|
30
|
+
if (!raw)
|
|
31
|
+
return fallback;
|
|
32
|
+
if (raw.endsWith('/api/v1'))
|
|
33
|
+
return raw;
|
|
34
|
+
if (raw.endsWith('/api'))
|
|
35
|
+
return `${raw}/v1`;
|
|
36
|
+
return `${raw}/api/v1`;
|
|
37
|
+
}
|
|
38
|
+
function resolveApiKey() {
|
|
39
|
+
const env = process.env.MAGNETO_API_KEY?.trim();
|
|
40
|
+
if (env)
|
|
41
|
+
return env;
|
|
42
|
+
try {
|
|
43
|
+
const file = path.join(os.homedir(), '.magneto', 'credentials.json');
|
|
44
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
45
|
+
if (parsed.api_key?.trim())
|
|
46
|
+
return parsed.api_key.trim();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
throw new Error('MAGNETO_API_KEY not set and ~/.magneto/credentials.json missing');
|
|
52
|
+
}
|
|
53
|
+
const API_BASE = normalizeApiBase();
|
|
54
|
+
let warnedMissingKey = false;
|
|
55
|
+
let cachedScopes = null;
|
|
56
|
+
let accountFetched = false;
|
|
57
|
+
function currentApiKey() {
|
|
58
|
+
try {
|
|
59
|
+
return resolveApiKey();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (!warnedMissingKey) {
|
|
63
|
+
warnedMissingKey = true;
|
|
64
|
+
process.stderr.write(`[magneto-mcp] warning: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function api(method, pathSuffix, body) {
|
|
70
|
+
const key = currentApiKey();
|
|
71
|
+
if (!key) {
|
|
72
|
+
return { ok: false, status: 401, data: { detail: 'MAGNETO_API_KEY not configured' } };
|
|
73
|
+
}
|
|
74
|
+
const res = await fetch(`${API_BASE}${pathSuffix}`, {
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${key}`,
|
|
78
|
+
Accept: 'application/json',
|
|
79
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
80
|
+
},
|
|
81
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
82
|
+
});
|
|
83
|
+
const text = await res.text();
|
|
84
|
+
let data = text;
|
|
85
|
+
try {
|
|
86
|
+
data = text ? JSON.parse(text) : null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// keep text
|
|
90
|
+
}
|
|
91
|
+
return { ok: res.ok, status: res.status, data };
|
|
92
|
+
}
|
|
93
|
+
function listedTools() {
|
|
94
|
+
const source = cachedScopes != null ? listToolsForScopes(cachedScopes) : MCP_TOOL_CATALOG;
|
|
95
|
+
return source.map(({ name, description, inputSchema }) => ({
|
|
96
|
+
name,
|
|
97
|
+
description,
|
|
98
|
+
inputSchema,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
async function ensureScopeCache() {
|
|
102
|
+
if (accountFetched)
|
|
103
|
+
return;
|
|
104
|
+
accountFetched = true;
|
|
105
|
+
const r = await api('GET', '/account');
|
|
106
|
+
if (r.ok && r.data && typeof r.data === 'object') {
|
|
107
|
+
const scopes = r.data.key?.scopes;
|
|
108
|
+
if (Array.isArray(scopes) && scopes.every((s) => typeof s === 'string')) {
|
|
109
|
+
cachedScopes = scopes;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
cachedScopes = null;
|
|
114
|
+
process.stderr.write('[magneto-mcp] warning: GET /account failed; listing all tools (per-call errors still apply)\n');
|
|
115
|
+
}
|
|
116
|
+
function toolText(payload, isError = false) {
|
|
117
|
+
const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
|
|
118
|
+
return { content: [{ type: 'text', text }], isError };
|
|
119
|
+
}
|
|
120
|
+
export async function callTool(name, args) {
|
|
121
|
+
const def = findToolDef(name);
|
|
122
|
+
if (cachedScopes != null && def && !cachedScopes.includes(def.requiredScope)) {
|
|
123
|
+
return toolText(missingScopePayload(def.requiredScope), true);
|
|
124
|
+
}
|
|
125
|
+
return callToolHttp(api, name, args);
|
|
126
|
+
}
|
|
127
|
+
export async function handleRpc(msg) {
|
|
128
|
+
const id = msg.id ?? null;
|
|
129
|
+
if (msg.method?.startsWith('notifications/'))
|
|
130
|
+
return null;
|
|
131
|
+
switch (msg.method) {
|
|
132
|
+
case 'initialize':
|
|
133
|
+
return {
|
|
134
|
+
jsonrpc: '2.0',
|
|
135
|
+
id,
|
|
136
|
+
result: {
|
|
137
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
138
|
+
capabilities: { tools: {} },
|
|
139
|
+
serverInfo: { name: 'magneto-mcp', version: MCP_SERVER_VERSION },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
case 'ping':
|
|
143
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
144
|
+
case 'tools/list':
|
|
145
|
+
await ensureScopeCache();
|
|
146
|
+
return { jsonrpc: '2.0', id, result: { tools: listedTools() } };
|
|
147
|
+
case 'tools/call': {
|
|
148
|
+
const name = String(msg.params?.name ?? '');
|
|
149
|
+
const args = msg.params?.arguments ?? {};
|
|
150
|
+
const result = await callTool(name, args);
|
|
151
|
+
return { jsonrpc: '2.0', id, result };
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return {
|
|
155
|
+
jsonrpc: '2.0',
|
|
156
|
+
id,
|
|
157
|
+
error: { code: -32601, message: `Method not found: ${msg.method}` },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function resetStdioState() {
|
|
162
|
+
cachedScopes = null;
|
|
163
|
+
accountFetched = false;
|
|
164
|
+
warnedMissingKey = false;
|
|
165
|
+
}
|
|
166
|
+
function writeMessage(msg) {
|
|
167
|
+
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
168
|
+
}
|
|
169
|
+
function isMain() {
|
|
170
|
+
const entry = process.argv[1];
|
|
171
|
+
if (!entry)
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
return import.meta.url === pathToFileURL(path.resolve(entry)).href;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function startStdio() {
|
|
181
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
182
|
+
rl.on('line', (line) => {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (!trimmed)
|
|
185
|
+
return;
|
|
186
|
+
let msg;
|
|
187
|
+
try {
|
|
188
|
+
msg = JSON.parse(trimmed);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
writeMessage({
|
|
192
|
+
jsonrpc: '2.0',
|
|
193
|
+
id: null,
|
|
194
|
+
error: { code: -32700, message: 'Parse error' },
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
void handleRpc(msg)
|
|
199
|
+
.then((result) => {
|
|
200
|
+
if (result != null)
|
|
201
|
+
writeMessage(result);
|
|
202
|
+
})
|
|
203
|
+
.catch((err) => {
|
|
204
|
+
process.stderr.write(`[magneto-mcp] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (isMain())
|
|
209
|
+
startStdio();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
2
|
+
export declare const MCP_SERVER_VERSION: string;
|
|
3
|
+
export type McpToolKind = 'both' | 'desktop-gui' | 'desktop-cu' | 'agent-ok';
|
|
4
|
+
export type McpToolDef = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: Record<string, unknown>;
|
|
8
|
+
requiredScope: 'computers' | 'exec' | 'runs' | 'files' | 'catalog' | 'account';
|
|
9
|
+
kinds: McpToolKind;
|
|
10
|
+
specMethod: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
11
|
+
specPath: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const DESKTOP_ACTION_ENUM: readonly ["screenshot", "click", "double_click", "right_click", "move", "type", "key", "scroll", "drag", "wait"];
|
|
14
|
+
export declare const MCP_TOOL_CATALOG: McpToolDef[];
|
|
15
|
+
export declare function listToolsForScopes(scopes: readonly string[]): McpToolDef[];
|
|
16
|
+
export declare function findToolDef(name: string): McpToolDef | undefined;
|
|
17
|
+
export declare function missingScopePayload(requiredScope: string): {
|
|
18
|
+
detail: string;
|
|
19
|
+
status: 403;
|
|
20
|
+
};
|
|
21
|
+
export declare const MCP_TOOL_NAMES: string[];
|
|
22
|
+
export type McpToolName = (typeof MCP_TOOL_NAMES)[number];
|
|
23
|
+
export declare const MCP_TOOL_SCOPES: Record<string, McpToolDef['requiredScope']>;
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Plain-data MCP tool catalog (#439). Duplicate of app/client/src/lib/mcp/tool-catalog.ts.
|
|
2
|
+
// Zero Worker imports. Keep in lockstep (registry-parity.test.ts).
|
|
3
|
+
import { readPackageVersion } from './package-version.js';
|
|
4
|
+
export const MCP_PROTOCOL_VERSION = '2025-03-26';
|
|
5
|
+
export const MCP_SERVER_VERSION = readPackageVersion();
|
|
6
|
+
export const DESKTOP_ACTION_ENUM = [
|
|
7
|
+
'screenshot',
|
|
8
|
+
'click',
|
|
9
|
+
'double_click',
|
|
10
|
+
'right_click',
|
|
11
|
+
'move',
|
|
12
|
+
'type',
|
|
13
|
+
'key',
|
|
14
|
+
'scroll',
|
|
15
|
+
'drag',
|
|
16
|
+
'wait',
|
|
17
|
+
];
|
|
18
|
+
export const MCP_TOOL_CATALOG = [
|
|
19
|
+
{
|
|
20
|
+
name: 'create',
|
|
21
|
+
description: 'Create a Magneto computer in the API key workspace. Scope computers. Works for both desktop and agent flavors (default flavor is agent; use flavor=desktop for CU machines). workspace_id is forced from the key; client-supplied workspace_id is ignored.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
name: { type: 'string', description: 'Optional display name' },
|
|
26
|
+
flavor: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: ['agent', 'desktop'],
|
|
29
|
+
description: 'Computer flavor (default agent)',
|
|
30
|
+
},
|
|
31
|
+
template_id: { type: 'number', description: 'Optional agent template id' },
|
|
32
|
+
vcpu: { type: 'number' },
|
|
33
|
+
ram_gb: { type: 'number' },
|
|
34
|
+
disk_gb: { type: 'number' },
|
|
35
|
+
image: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
requiredScope: 'computers',
|
|
39
|
+
kinds: 'both',
|
|
40
|
+
specMethod: 'post',
|
|
41
|
+
specPath: '/computers',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'list',
|
|
45
|
+
description: 'List computers for the API key minting user. Scope computers. Returns both desktop and agent computers.',
|
|
46
|
+
inputSchema: { type: 'object', properties: {} },
|
|
47
|
+
requiredScope: 'computers',
|
|
48
|
+
kinds: 'both',
|
|
49
|
+
specMethod: 'get',
|
|
50
|
+
specPath: '/computers',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'get',
|
|
54
|
+
description: 'Get one computer by id. Scope computers. Works for both desktop and agent computers.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
required: ['computer_id'],
|
|
58
|
+
properties: { computer_id: { type: 'string' } },
|
|
59
|
+
},
|
|
60
|
+
requiredScope: 'computers',
|
|
61
|
+
kinds: 'both',
|
|
62
|
+
specMethod: 'get',
|
|
63
|
+
specPath: '/computers/{id}',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'start',
|
|
67
|
+
description: 'Start a stopped computer. Scope computers. Works for both desktop and agent computers.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
required: ['computer_id'],
|
|
71
|
+
properties: { computer_id: { type: 'string' } },
|
|
72
|
+
},
|
|
73
|
+
requiredScope: 'computers',
|
|
74
|
+
kinds: 'both',
|
|
75
|
+
specMethod: 'post',
|
|
76
|
+
specPath: '/computers/{id}/start',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'stop',
|
|
80
|
+
description: 'Stop a computer. Scope computers. Works for both desktop and agent computers.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
required: ['computer_id'],
|
|
84
|
+
properties: { computer_id: { type: 'string' } },
|
|
85
|
+
},
|
|
86
|
+
requiredScope: 'computers',
|
|
87
|
+
kinds: 'both',
|
|
88
|
+
specMethod: 'post',
|
|
89
|
+
specPath: '/computers/{id}/stop',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'restart',
|
|
93
|
+
description: 'Restart a running or degraded computer. Scope computers. Works for both desktop and agent computers.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
required: ['computer_id'],
|
|
97
|
+
properties: { computer_id: { type: 'string' } },
|
|
98
|
+
},
|
|
99
|
+
requiredScope: 'computers',
|
|
100
|
+
kinds: 'both',
|
|
101
|
+
specMethod: 'post',
|
|
102
|
+
specPath: '/computers/{id}/restart',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'delete',
|
|
106
|
+
description: 'DESTRUCTIVE. Irreversible deprovision. confirm must equal the computer name. Scope computers. Works for both desktop and agent computers.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
required: ['computer_id', 'confirm'],
|
|
110
|
+
properties: {
|
|
111
|
+
computer_id: { type: 'string' },
|
|
112
|
+
confirm: { type: 'string', description: 'Must equal the computer name' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
requiredScope: 'computers',
|
|
116
|
+
kinds: 'both',
|
|
117
|
+
specMethod: 'delete',
|
|
118
|
+
specPath: '/computers/{id}',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'resize',
|
|
122
|
+
description: 'Resize a desktop computer (grow-only: vcpu, ram_gb, disk_gb). Scope computers. Desktop grow-only; agent or shrink returns 422 passthrough.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
required: ['computer_id'],
|
|
126
|
+
properties: {
|
|
127
|
+
computer_id: { type: 'string' },
|
|
128
|
+
vcpu: { type: 'number' },
|
|
129
|
+
ram_gb: { type: 'number' },
|
|
130
|
+
disk_gb: { type: 'number' },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
requiredScope: 'computers',
|
|
134
|
+
kinds: 'both',
|
|
135
|
+
specMethod: 'post',
|
|
136
|
+
specPath: '/computers/{id}/resize',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'uptime',
|
|
140
|
+
description: 'Get Fly Prometheus uptime for a computer. Scope computers. Works for both desktop and agent computers. Optional window_seconds.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
required: ['computer_id'],
|
|
144
|
+
properties: {
|
|
145
|
+
computer_id: { type: 'string' },
|
|
146
|
+
window_seconds: { type: 'number' },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
requiredScope: 'computers',
|
|
150
|
+
kinds: 'both',
|
|
151
|
+
specMethod: 'get',
|
|
152
|
+
specPath: '/computers/{id}/uptime',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'gateway_url',
|
|
156
|
+
description: 'Mint a 60-second signed gateway URL. Scope computers. Desktop uses /desk; agent uses /connect. target=terminal is desktop-only. Do not log the url.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
required: ['computer_id'],
|
|
160
|
+
properties: {
|
|
161
|
+
computer_id: { type: 'string' },
|
|
162
|
+
target: { type: 'string', enum: ['desktop', 'terminal'] },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
requiredScope: 'computers',
|
|
166
|
+
kinds: 'both',
|
|
167
|
+
specMethod: 'post',
|
|
168
|
+
specPath: '/computers/{id}/gateway',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'screenshot',
|
|
172
|
+
description: 'Take a screenshot of a desktop computer. Scope computers. Desktop-gui: real screenshot. Agent computers return structured status (screenshot: null, no action call).',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
required: ['computer_id'],
|
|
176
|
+
properties: { computer_id: { type: 'string', description: 'Computer id' } },
|
|
177
|
+
},
|
|
178
|
+
requiredScope: 'computers',
|
|
179
|
+
kinds: 'desktop-gui',
|
|
180
|
+
specMethod: 'post',
|
|
181
|
+
specPath: '/computers/{id}/actions/{action}',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'action',
|
|
185
|
+
description: 'Dispatch one desktop GUI action (screenshot, click, double_click, right_click, move, type, key, scroll, drag, wait). Scope computers. Desktop-gui only. Agent computers return structured status (action: null, no helper call).',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
required: ['computer_id', 'action'],
|
|
189
|
+
properties: {
|
|
190
|
+
computer_id: { type: 'string' },
|
|
191
|
+
action: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
enum: [
|
|
194
|
+
'screenshot',
|
|
195
|
+
'click',
|
|
196
|
+
'double_click',
|
|
197
|
+
'right_click',
|
|
198
|
+
'move',
|
|
199
|
+
'type',
|
|
200
|
+
'key',
|
|
201
|
+
'scroll',
|
|
202
|
+
'drag',
|
|
203
|
+
'wait',
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
params: { type: 'object', additionalProperties: true },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
requiredScope: 'computers',
|
|
210
|
+
kinds: 'desktop-gui',
|
|
211
|
+
specMethod: 'post',
|
|
212
|
+
specPath: '/computers/{id}/actions/{action}',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'bash',
|
|
216
|
+
description: 'Run a shell command on a computer. Scope exec (opt-in, not in DEFAULT_SCOPES). Allowed on desktop and agent. Output capped 1 MiB; timeout 1–300 s.',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
required: ['computer_id', 'command'],
|
|
220
|
+
properties: {
|
|
221
|
+
computer_id: { type: 'string' },
|
|
222
|
+
command: { type: 'string' },
|
|
223
|
+
timeout: { type: 'number' },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
requiredScope: 'exec',
|
|
227
|
+
kinds: 'both',
|
|
228
|
+
specMethod: 'post',
|
|
229
|
+
specPath: '/computers/{id}/bash',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'run',
|
|
233
|
+
description: 'Run a computer-use instruction on a desktop computer (streaming CU loop). Scope runs. Desktop only. Burns the platform CU spend cap or the single named BYO secret. Concurrent-run ceiling applies (429). Agent computers return 422.',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
required: ['computer_id', 'instruction'],
|
|
237
|
+
properties: {
|
|
238
|
+
computer_id: { type: 'string' },
|
|
239
|
+
instruction: { type: 'string' },
|
|
240
|
+
model: { type: 'string' },
|
|
241
|
+
max_steps: { type: 'number' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
requiredScope: 'runs',
|
|
245
|
+
kinds: 'desktop-cu',
|
|
246
|
+
specMethod: 'post',
|
|
247
|
+
specPath: '/computers/{id}/computer-use/run',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'stop_run',
|
|
251
|
+
description: 'Hard-stop a computer-use run. Scope runs. Desktop-cu. Body run_id. 404 if already finished.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
required: ['computer_id', 'run_id'],
|
|
255
|
+
properties: {
|
|
256
|
+
computer_id: { type: 'string' },
|
|
257
|
+
run_id: { type: 'string' },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
requiredScope: 'runs',
|
|
261
|
+
kinds: 'desktop-cu',
|
|
262
|
+
specMethod: 'post',
|
|
263
|
+
specPath: '/computers/{id}/computer-use/stop',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'list_runs',
|
|
267
|
+
description: 'List computer-use run history for a computer. Scope runs. Desktop-cu. Optional limit and offset.',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
required: ['computer_id'],
|
|
271
|
+
properties: {
|
|
272
|
+
computer_id: { type: 'string' },
|
|
273
|
+
limit: { type: 'number' },
|
|
274
|
+
offset: { type: 'number' },
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
requiredScope: 'runs',
|
|
278
|
+
kinds: 'desktop-cu',
|
|
279
|
+
specMethod: 'get',
|
|
280
|
+
specPath: '/computers/{id}/runs',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'get_run',
|
|
284
|
+
description: 'Get a computer-use run and its step events. Scope runs. Desktop-cu. Signed screenshot URLs are credentials — do not log them.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
required: ['computer_id', 'run_id'],
|
|
288
|
+
properties: {
|
|
289
|
+
computer_id: { type: 'string' },
|
|
290
|
+
run_id: { type: 'string' },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
requiredScope: 'runs',
|
|
294
|
+
kinds: 'desktop-cu',
|
|
295
|
+
specMethod: 'get',
|
|
296
|
+
specPath: '/computers/{id}/runs/{runId}',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'install_skill',
|
|
300
|
+
description: 'Install a marketplace skill onto a computer by numeric skill_id. Scope computers. Works for both desktop and agent computers.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
required: ['computer_id', 'skill_id'],
|
|
304
|
+
properties: {
|
|
305
|
+
computer_id: { type: 'string' },
|
|
306
|
+
skill_id: { type: 'number' },
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
requiredScope: 'computers',
|
|
310
|
+
kinds: 'both',
|
|
311
|
+
specMethod: 'post',
|
|
312
|
+
specPath: '/computers/{id}/skills',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: 'uninstall_skill',
|
|
316
|
+
description: 'Uninstall a marketplace skill from a computer. Scope computers. Works for both desktop and agent computers. Missing install is a no-op.',
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
required: ['computer_id', 'skill_id'],
|
|
320
|
+
properties: {
|
|
321
|
+
computer_id: { type: 'string' },
|
|
322
|
+
skill_id: { type: 'number' },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
requiredScope: 'computers',
|
|
326
|
+
kinds: 'both',
|
|
327
|
+
specMethod: 'delete',
|
|
328
|
+
specPath: '/computers/{id}/skills/{skillId}',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'list_skills',
|
|
332
|
+
description: 'List marketplace skills (GET /skills). Scope catalog. Not the per-computer installed list. REST has no search query.',
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
skip: { type: 'number' },
|
|
337
|
+
limit: { type: 'number' },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
requiredScope: 'catalog',
|
|
341
|
+
kinds: 'both',
|
|
342
|
+
specMethod: 'get',
|
|
343
|
+
specPath: '/skills',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'search_marketplace',
|
|
347
|
+
description: 'Search the marketplace skill page in-tool (name/description filter). Scope catalog. REST has no ?q= — this is a tool-side filter over GET /skills. A numeric query also fetches GET /skills/{id}.',
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
properties: {
|
|
351
|
+
query: { type: 'string' },
|
|
352
|
+
skip: { type: 'number' },
|
|
353
|
+
limit: { type: 'number' },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
requiredScope: 'catalog',
|
|
357
|
+
kinds: 'both',
|
|
358
|
+
specMethod: 'get',
|
|
359
|
+
specPath: '/skills',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'list_templates',
|
|
363
|
+
description: 'List marketplace computer templates. Scope catalog.',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
skip: { type: 'number' },
|
|
368
|
+
limit: { type: 'number' },
|
|
369
|
+
category: { type: 'string' },
|
|
370
|
+
featured: { type: 'boolean' },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
requiredScope: 'catalog',
|
|
374
|
+
kinds: 'both',
|
|
375
|
+
specMethod: 'get',
|
|
376
|
+
specPath: '/templates',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'list_files',
|
|
380
|
+
description: 'List workspace files. Scope files.',
|
|
381
|
+
inputSchema: { type: 'object', properties: {} },
|
|
382
|
+
requiredScope: 'files',
|
|
383
|
+
kinds: 'both',
|
|
384
|
+
specMethod: 'get',
|
|
385
|
+
specPath: '/files',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: 'account',
|
|
389
|
+
description: 'Read-only self-budget snapshot (workspace, subscription, CU/LLM usage, key scopes). Scope account.',
|
|
390
|
+
inputSchema: { type: 'object', properties: {} },
|
|
391
|
+
requiredScope: 'account',
|
|
392
|
+
kinds: 'both',
|
|
393
|
+
specMethod: 'get',
|
|
394
|
+
specPath: '/account',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'chat',
|
|
398
|
+
description: 'OpenAI-compatible chat completion over computer-use (non-stream). Scope runs. Desktop only. Burns the platform CU spend cap or the single named BYO secret. Concurrent-run ceiling applies (429). Accepts messages or a single instruction. stream is coerced to false. Agent computers return 422.',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
required: ['computer_id'],
|
|
402
|
+
properties: {
|
|
403
|
+
computer_id: { type: 'string' },
|
|
404
|
+
messages: { type: 'array', items: { type: 'object' } },
|
|
405
|
+
instruction: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: 'Shorthand: wrapped as a single user message when messages is omitted',
|
|
408
|
+
},
|
|
409
|
+
model: { type: 'string' },
|
|
410
|
+
max_steps: { type: 'number' },
|
|
411
|
+
include_thinking: { type: 'boolean' },
|
|
412
|
+
stream: { type: 'boolean', description: 'Ignored; MCP always runs non-stream' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
requiredScope: 'runs',
|
|
416
|
+
kinds: 'desktop-cu',
|
|
417
|
+
specMethod: 'post',
|
|
418
|
+
specPath: '/chat/completions',
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
export function listToolsForScopes(scopes) {
|
|
422
|
+
return MCP_TOOL_CATALOG.filter((tool) => scopes.includes(tool.requiredScope));
|
|
423
|
+
}
|
|
424
|
+
export function findToolDef(name) {
|
|
425
|
+
return MCP_TOOL_CATALOG.find((tool) => tool.name === name);
|
|
426
|
+
}
|
|
427
|
+
export function missingScopePayload(requiredScope) {
|
|
428
|
+
return { detail: `Missing scope: ${requiredScope}`, status: 403 };
|
|
429
|
+
}
|
|
430
|
+
export const MCP_TOOL_NAMES = MCP_TOOL_CATALOG.map((tool) => tool.name);
|
|
431
|
+
export const MCP_TOOL_SCOPES = Object.fromEntries(MCP_TOOL_CATALOG.map((tool) => [tool.name, tool.requiredScope]));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ApiFn = (method: string, pathSuffix: string, body?: unknown) => Promise<{
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
data: unknown;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function callToolHttp(api: ApiFn, name: string, args: Record<string, unknown>): Promise<{
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
isError?: boolean;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Stdio MCP HTTP map (#439). Speaks /api/v1 only — no Worker imports.
|
|
2
|
+
import { findToolDef, missingScopePayload } from './tool-catalog.js';
|
|
3
|
+
function toolText(payload, isError = false) {
|
|
4
|
+
const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
|
|
5
|
+
return { content: [{ type: 'text', text }], isError };
|
|
6
|
+
}
|
|
7
|
+
function kindOf(data) {
|
|
8
|
+
return String(data?.kind ?? 'agent').toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
function enc(value) {
|
|
11
|
+
return encodeURIComponent(value);
|
|
12
|
+
}
|
|
13
|
+
export async function callToolHttp(api, name, args) {
|
|
14
|
+
const def = findToolDef(name);
|
|
15
|
+
if (!def)
|
|
16
|
+
return toolText({ detail: `Unknown tool: ${name}` }, true);
|
|
17
|
+
switch (name) {
|
|
18
|
+
case 'list': {
|
|
19
|
+
const r = await api('GET', '/computers');
|
|
20
|
+
return toolText(r.data, !r.ok);
|
|
21
|
+
}
|
|
22
|
+
case 'create': {
|
|
23
|
+
const body = {};
|
|
24
|
+
if (args.name != null)
|
|
25
|
+
body.name = args.name;
|
|
26
|
+
if (args.flavor != null)
|
|
27
|
+
body.flavor = args.flavor;
|
|
28
|
+
if (args.template_id != null)
|
|
29
|
+
body.template_id = args.template_id;
|
|
30
|
+
if (args.vcpu != null)
|
|
31
|
+
body.vcpu = args.vcpu;
|
|
32
|
+
if (args.ram_gb != null)
|
|
33
|
+
body.ram_gb = args.ram_gb;
|
|
34
|
+
if (args.disk_gb != null)
|
|
35
|
+
body.disk_gb = args.disk_gb;
|
|
36
|
+
if (args.image != null)
|
|
37
|
+
body.image = args.image;
|
|
38
|
+
const r = await api('POST', '/computers', body);
|
|
39
|
+
return toolText(r.data, !r.ok);
|
|
40
|
+
}
|
|
41
|
+
case 'get': {
|
|
42
|
+
const r = await api('GET', `/computers/${enc(String(args.computer_id ?? ''))}`);
|
|
43
|
+
return toolText(r.data, !r.ok);
|
|
44
|
+
}
|
|
45
|
+
case 'start': {
|
|
46
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/start`);
|
|
47
|
+
return toolText(r.data, !r.ok);
|
|
48
|
+
}
|
|
49
|
+
case 'stop': {
|
|
50
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/stop`);
|
|
51
|
+
return toolText(r.data, !r.ok);
|
|
52
|
+
}
|
|
53
|
+
case 'restart': {
|
|
54
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/restart`);
|
|
55
|
+
return toolText(r.data, !r.ok);
|
|
56
|
+
}
|
|
57
|
+
case 'delete': {
|
|
58
|
+
if (!args.confirm || typeof args.confirm !== 'string') {
|
|
59
|
+
return toolText({ detail: 'confirm is required and must equal the computer name', status: 400 }, true);
|
|
60
|
+
}
|
|
61
|
+
const r = await api('DELETE', `/computers/${enc(String(args.computer_id ?? ''))}?confirm=${enc(args.confirm)}`);
|
|
62
|
+
return toolText(r.data, !r.ok);
|
|
63
|
+
}
|
|
64
|
+
case 'resize': {
|
|
65
|
+
const body = {};
|
|
66
|
+
if (args.vcpu != null)
|
|
67
|
+
body.vcpu = args.vcpu;
|
|
68
|
+
if (args.ram_gb != null)
|
|
69
|
+
body.ram_gb = args.ram_gb;
|
|
70
|
+
if (args.disk_gb != null)
|
|
71
|
+
body.disk_gb = args.disk_gb;
|
|
72
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/resize`, body);
|
|
73
|
+
return toolText(r.data, !r.ok);
|
|
74
|
+
}
|
|
75
|
+
case 'uptime': {
|
|
76
|
+
const qs = typeof args.window_seconds === 'number' ? `?window_seconds=${args.window_seconds}` : '';
|
|
77
|
+
const r = await api('GET', `/computers/${enc(String(args.computer_id ?? ''))}/uptime${qs}`);
|
|
78
|
+
return toolText(r.data, !r.ok);
|
|
79
|
+
}
|
|
80
|
+
case 'gateway_url': {
|
|
81
|
+
const target = typeof args.target === 'string' ? args.target : 'desktop';
|
|
82
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/gateway?target=${enc(target)}`);
|
|
83
|
+
return toolText(r.data, !r.ok);
|
|
84
|
+
}
|
|
85
|
+
case 'screenshot': {
|
|
86
|
+
const id = String(args.computer_id ?? '');
|
|
87
|
+
const detail = await api('GET', `/computers/${enc(id)}`);
|
|
88
|
+
if (!detail.ok)
|
|
89
|
+
return toolText(detail.data, true);
|
|
90
|
+
if (kindOf(detail.data) !== 'desktop') {
|
|
91
|
+
return toolText({
|
|
92
|
+
kind: kindOf(detail.data),
|
|
93
|
+
state: detail.data?.state ?? null,
|
|
94
|
+
id,
|
|
95
|
+
message: 'Screenshot only for desktop computers; agent status returned.',
|
|
96
|
+
screenshot: null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const r = await api('POST', `/computers/${enc(id)}/actions/screenshot`, { params: {} });
|
|
100
|
+
return toolText(r.data, !r.ok);
|
|
101
|
+
}
|
|
102
|
+
case 'action': {
|
|
103
|
+
const id = String(args.computer_id ?? '');
|
|
104
|
+
const action = String(args.action ?? '');
|
|
105
|
+
const allowed = [
|
|
106
|
+
'screenshot',
|
|
107
|
+
'click',
|
|
108
|
+
'double_click',
|
|
109
|
+
'right_click',
|
|
110
|
+
'move',
|
|
111
|
+
'type',
|
|
112
|
+
'key',
|
|
113
|
+
'scroll',
|
|
114
|
+
'drag',
|
|
115
|
+
'wait',
|
|
116
|
+
];
|
|
117
|
+
if (!allowed.includes(action)) {
|
|
118
|
+
return toolText({ detail: 'Invalid action', status: 400 }, true);
|
|
119
|
+
}
|
|
120
|
+
const detail = await api('GET', `/computers/${enc(id)}`);
|
|
121
|
+
if (!detail.ok)
|
|
122
|
+
return toolText(detail.data, true);
|
|
123
|
+
if (kindOf(detail.data) !== 'desktop') {
|
|
124
|
+
return toolText({
|
|
125
|
+
kind: kindOf(detail.data),
|
|
126
|
+
state: detail.data?.state ?? null,
|
|
127
|
+
id,
|
|
128
|
+
message: 'Desktop GUI actions are only available for desktop computers.',
|
|
129
|
+
action: null,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const params = args.params && typeof args.params === 'object' && !Array.isArray(args.params)
|
|
133
|
+
? args.params
|
|
134
|
+
: {};
|
|
135
|
+
const r = await api('POST', `/computers/${enc(id)}/actions/${enc(action)}`, { params });
|
|
136
|
+
return toolText(r.data, !r.ok);
|
|
137
|
+
}
|
|
138
|
+
case 'bash': {
|
|
139
|
+
const body = { command: args.command };
|
|
140
|
+
if (args.timeout != null)
|
|
141
|
+
body.timeout = args.timeout;
|
|
142
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/bash`, body);
|
|
143
|
+
return toolText(r.data, !r.ok);
|
|
144
|
+
}
|
|
145
|
+
case 'run': {
|
|
146
|
+
const id = String(args.computer_id ?? '');
|
|
147
|
+
const detail = await api('GET', `/computers/${enc(id)}`);
|
|
148
|
+
if (!detail.ok)
|
|
149
|
+
return toolText(detail.data, true);
|
|
150
|
+
if (kindOf(detail.data) !== 'desktop') {
|
|
151
|
+
return toolText({
|
|
152
|
+
detail: 'computer-use/run is only supported on desktop computers. Agent machines use the agent chat/gateway surface instead.',
|
|
153
|
+
}, true);
|
|
154
|
+
}
|
|
155
|
+
const body = { instruction: args.instruction };
|
|
156
|
+
if (args.model != null)
|
|
157
|
+
body.model = args.model;
|
|
158
|
+
if (args.max_steps != null)
|
|
159
|
+
body.max_steps = args.max_steps;
|
|
160
|
+
const r = await api('POST', `/computers/${enc(id)}/computer-use/run`, body);
|
|
161
|
+
return toolText(r.data, !r.ok);
|
|
162
|
+
}
|
|
163
|
+
case 'stop_run': {
|
|
164
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/computer-use/stop`, { run_id: args.run_id });
|
|
165
|
+
return toolText(r.data, !r.ok);
|
|
166
|
+
}
|
|
167
|
+
case 'list_runs': {
|
|
168
|
+
const params = new URLSearchParams();
|
|
169
|
+
if (typeof args.limit === 'number')
|
|
170
|
+
params.set('limit', String(args.limit));
|
|
171
|
+
if (typeof args.offset === 'number')
|
|
172
|
+
params.set('offset', String(args.offset));
|
|
173
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
174
|
+
const r = await api('GET', `/computers/${enc(String(args.computer_id ?? ''))}/runs${qs}`);
|
|
175
|
+
return toolText(r.data, !r.ok);
|
|
176
|
+
}
|
|
177
|
+
case 'get_run': {
|
|
178
|
+
const r = await api('GET', `/computers/${enc(String(args.computer_id ?? ''))}/runs/${enc(String(args.run_id ?? ''))}`);
|
|
179
|
+
return toolText(r.data, !r.ok);
|
|
180
|
+
}
|
|
181
|
+
case 'install_skill': {
|
|
182
|
+
const r = await api('POST', `/computers/${enc(String(args.computer_id ?? ''))}/skills`, {
|
|
183
|
+
skill_id: Number(args.skill_id),
|
|
184
|
+
});
|
|
185
|
+
return toolText(r.data, !r.ok);
|
|
186
|
+
}
|
|
187
|
+
case 'uninstall_skill': {
|
|
188
|
+
const r = await api('DELETE', `/computers/${enc(String(args.computer_id ?? ''))}/skills/${enc(String(args.skill_id ?? ''))}`);
|
|
189
|
+
return toolText(r.data, !r.ok);
|
|
190
|
+
}
|
|
191
|
+
case 'list_skills': {
|
|
192
|
+
const params = new URLSearchParams();
|
|
193
|
+
if (typeof args.skip === 'number')
|
|
194
|
+
params.set('skip', String(args.skip));
|
|
195
|
+
if (typeof args.limit === 'number')
|
|
196
|
+
params.set('limit', String(args.limit));
|
|
197
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
198
|
+
const r = await api('GET', `/skills${qs}`);
|
|
199
|
+
return toolText(r.data, !r.ok);
|
|
200
|
+
}
|
|
201
|
+
case 'search_marketplace': {
|
|
202
|
+
const params = new URLSearchParams();
|
|
203
|
+
if (typeof args.skip === 'number')
|
|
204
|
+
params.set('skip', String(args.skip));
|
|
205
|
+
if (typeof args.limit === 'number')
|
|
206
|
+
params.set('limit', String(args.limit));
|
|
207
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
208
|
+
const page = await api('GET', `/skills${qs}`);
|
|
209
|
+
if (!page.ok)
|
|
210
|
+
return toolText(page.data, true);
|
|
211
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
212
|
+
const rows = Array.isArray(page.data) ? page.data : [];
|
|
213
|
+
const needle = query.toLowerCase();
|
|
214
|
+
const items = query
|
|
215
|
+
? rows.filter((row) => {
|
|
216
|
+
const rec = row;
|
|
217
|
+
return (String(rec.name ?? '')
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.includes(needle) ||
|
|
220
|
+
String(rec.description ?? '')
|
|
221
|
+
.toLowerCase()
|
|
222
|
+
.includes(needle));
|
|
223
|
+
})
|
|
224
|
+
: rows;
|
|
225
|
+
const numeric = Number(query);
|
|
226
|
+
if (query !== '' && Number.isFinite(numeric) && Number.isInteger(numeric)) {
|
|
227
|
+
const exact = await api('GET', `/skills/${enc(String(numeric))}`);
|
|
228
|
+
return toolText({ items, exact: exact.ok ? exact.data : null });
|
|
229
|
+
}
|
|
230
|
+
return toolText({ items });
|
|
231
|
+
}
|
|
232
|
+
case 'list_templates': {
|
|
233
|
+
const params = new URLSearchParams();
|
|
234
|
+
if (typeof args.skip === 'number')
|
|
235
|
+
params.set('skip', String(args.skip));
|
|
236
|
+
if (typeof args.limit === 'number')
|
|
237
|
+
params.set('limit', String(args.limit));
|
|
238
|
+
if (typeof args.category === 'string')
|
|
239
|
+
params.set('category', args.category);
|
|
240
|
+
if (typeof args.featured === 'boolean')
|
|
241
|
+
params.set('featured', String(args.featured));
|
|
242
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
243
|
+
const r = await api('GET', `/templates${qs}`);
|
|
244
|
+
return toolText(r.data, !r.ok);
|
|
245
|
+
}
|
|
246
|
+
case 'list_files': {
|
|
247
|
+
const r = await api('GET', '/files');
|
|
248
|
+
return toolText(r.data, !r.ok);
|
|
249
|
+
}
|
|
250
|
+
case 'account': {
|
|
251
|
+
const r = await api('GET', '/account');
|
|
252
|
+
return toolText(r.data, !r.ok);
|
|
253
|
+
}
|
|
254
|
+
case 'chat': {
|
|
255
|
+
const id = String(args.computer_id ?? '');
|
|
256
|
+
const detail = await api('GET', `/computers/${enc(id)}`);
|
|
257
|
+
if (!detail.ok)
|
|
258
|
+
return toolText(detail.data, true);
|
|
259
|
+
if (kindOf(detail.data) !== 'desktop') {
|
|
260
|
+
return toolText({
|
|
261
|
+
detail: 'computer-use/run is only supported on desktop computers. Agent machines use the agent chat/gateway surface instead.',
|
|
262
|
+
}, true);
|
|
263
|
+
}
|
|
264
|
+
const body = { computer_id: id, stream: false };
|
|
265
|
+
if (Array.isArray(args.messages))
|
|
266
|
+
body.messages = args.messages;
|
|
267
|
+
else if (typeof args.instruction === 'string') {
|
|
268
|
+
body.messages = [{ role: 'user', content: args.instruction }];
|
|
269
|
+
}
|
|
270
|
+
if (args.model != null)
|
|
271
|
+
body.model = args.model;
|
|
272
|
+
if (args.max_steps != null)
|
|
273
|
+
body.max_steps = args.max_steps;
|
|
274
|
+
if (args.include_thinking != null)
|
|
275
|
+
body.include_thinking = args.include_thinking;
|
|
276
|
+
const r = await api('POST', '/chat/completions', body);
|
|
277
|
+
return toolText(r.data, !r.ok);
|
|
278
|
+
}
|
|
279
|
+
default:
|
|
280
|
+
return toolText(missingScopePayload(def.requiredScope), true);
|
|
281
|
+
}
|
|
282
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magnetoagents/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Magneto MCP stdio server — drive computers via sk_live_* against /api/v1 (or remote /mcp)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"magneto-mcp": "./dist/stdio.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/vargasjons/magneto-app.git",
|
|
26
|
+
"directory": "packages/mcp"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.17.0",
|
|
31
|
+
"typescript": "^5.8.2",
|
|
32
|
+
"vitest": "^3.0.5"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"private": false
|
|
38
|
+
}
|