@agentvalet/mcp-server 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.js +89 -0
- package/dist/context.js +8 -0
- package/dist/index.js +30 -719
- package/dist/net.js +80 -0
- package/dist/tools/handlers.js +272 -0
- package/dist/tools/schemas.js +272 -0
- package/package.json +47 -46
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// ── BOOT DIAGNOSTICS — must be FIRST executable code ─────────────────────
|
|
2
3
|
// Claude Desktop captures whatever the MCP server writes to stderr before
|
|
3
4
|
// the stdio transport handshake. If startup crashes silently (top-level
|
|
@@ -17,9 +18,12 @@ process.on("unhandledRejection", (err) => {
|
|
|
17
18
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
20
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
-
import { SignJWT } from "jose";
|
|
21
21
|
import { validateConfig } from "./config.js";
|
|
22
22
|
import { renderInstructions } from "./instructions.js";
|
|
23
|
+
import { signJWT } from "./auth.js";
|
|
24
|
+
import { ALLOWED_METHODS, ALL_TOOLS } from "./tools/schemas.js";
|
|
25
|
+
import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, } from "./tools/handlers.js";
|
|
26
|
+
import { errorContent } from "./net.js";
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Startup env validation
|
|
25
29
|
// ---------------------------------------------------------------------------
|
|
@@ -34,376 +38,41 @@ catch (err) {
|
|
|
34
38
|
const { agentId: AGENT_ID, ownerId: OWNER_ID, proxyUrl: PROXY_URL, privateKeyPem: AGENT_PRIVATE_KEY_RAW, privateKey } = configResult;
|
|
35
39
|
process.stderr.write(`[mcp-server] config ok | agent=${AGENT_ID} | owner=${OWNER_ID} | proxy=${PROXY_URL} | has_key=${!!privateKey}\n`);
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
|
-
//
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
async function signJWT() {
|
|
40
|
-
if (!privateKey)
|
|
41
|
-
throw new Error("Private key not loaded");
|
|
42
|
-
return new SignJWT({ agent_id: AGENT_ID, owner_id: OWNER_ID })
|
|
43
|
-
.setProtectedHeader({ alg: "RS256" })
|
|
44
|
-
.setIssuedAt()
|
|
45
|
-
.setExpirationTime("60s")
|
|
46
|
-
.sign(privateKey);
|
|
47
|
-
}
|
|
48
|
-
async function notifyBindSecret() {
|
|
49
|
-
try {
|
|
50
|
-
await fetchWithTimeout(`${PROXY_URL}/v1/bind-secret`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { "Content-Type": "application/json" },
|
|
53
|
-
body: JSON.stringify({ agent_id: AGENT_ID, owner_id: OWNER_ID }),
|
|
54
|
-
}, 8_000);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// best-effort — don't block the error response
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function pendingFirstCallResponse() {
|
|
61
|
-
return {
|
|
62
|
-
content: [{
|
|
63
|
-
type: "text",
|
|
64
|
-
text: JSON.stringify({
|
|
65
|
-
error: "Agent not yet activated — owner confirmation pending. Retry in 60 seconds.",
|
|
66
|
-
}),
|
|
67
|
-
}],
|
|
68
|
-
isError: true,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
// State C — no credentials at all (empty env / Glama-style sandbox). The
|
|
72
|
-
// server still boots and answers introspection; an authed tool call lands
|
|
73
|
-
// here and gets an actionable message instead of a crash. Distinct from the
|
|
74
|
-
// state-B "owner confirmation pending" response above, which means an identity
|
|
75
|
-
// IS configured but its key hasn't arrived yet.
|
|
76
|
-
function credentialsNotConfiguredResponse() {
|
|
77
|
-
return {
|
|
78
|
-
content: [{
|
|
79
|
-
type: "text",
|
|
80
|
-
text: "AgentValet credentials are not configured. Set AGENTVALET_AGENT_ID, " +
|
|
81
|
-
"AGENTVALET_OWNER_ID, and the agent private key (and optionally " +
|
|
82
|
-
"AGENTVALET_PROXY_URL). Run npx @agentvalet/register to create an agent. " +
|
|
83
|
-
"Docs: https://github.com/AgentValet/AgentValet#quickstart",
|
|
84
|
-
}],
|
|
85
|
-
isError: true,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
// Credential gate for authed tools/call. Returns null when the call may
|
|
89
|
-
// proceed (state A — a JWT can be signed), or a ready-to-return MCP tool
|
|
90
|
-
// result for the two no-key states:
|
|
91
|
-
// • state B (identity present, key pending) → existing invite-bind pending
|
|
92
|
-
// response, preserving the MCPB first-run flow.
|
|
93
|
-
// • state C (no identity at all) → credentials-not-configured.
|
|
94
|
-
// The no-auth tools (agent_register / agent_status) intentionally never call
|
|
95
|
-
// this — you must be able to register in order to OBTAIN credentials.
|
|
96
|
-
async function requireCredentials() {
|
|
97
|
-
if (AGENT_PRIVATE_KEY_RAW !== null)
|
|
98
|
-
return null;
|
|
99
|
-
if (AGENT_ID && OWNER_ID) {
|
|
100
|
-
await notifyBindSecret();
|
|
101
|
-
return pendingFirstCallResponse();
|
|
102
|
-
}
|
|
103
|
-
return credentialsNotConfiguredResponse();
|
|
104
|
-
}
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Tool definitions
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
109
|
-
const LIST_PLATFORMS_TOOL = {
|
|
110
|
-
name: "list_platforms",
|
|
111
|
-
description: "list_platforms: List the platforms and permission scopes this agent has access to.\nInput: None.\nReturns: { platforms: [{ platformId, platformName, scopes, requireApproval }], version: \"<hex>\" }.\nVersion: a deterministic hash that only changes when the platform set or scopes change. Cache the value across calls in the same session — only refresh when you suspect platforms have changed (e.g. user mentions a new connection).\nAuth: Bearer JWT.",
|
|
112
|
-
inputSchema: { type: "object", properties: {} },
|
|
113
|
-
outputSchema: {
|
|
114
|
-
type: "object",
|
|
115
|
-
properties: {
|
|
116
|
-
platforms: {
|
|
117
|
-
type: "array",
|
|
118
|
-
items: {
|
|
119
|
-
type: "object",
|
|
120
|
-
properties: {
|
|
121
|
-
platformId: { type: "string" },
|
|
122
|
-
platformName: { type: "string" },
|
|
123
|
-
scopes: { type: "array", items: { type: "string" } },
|
|
124
|
-
requireApproval: { type: "boolean" },
|
|
125
|
-
},
|
|
126
|
-
required: ["platformId", "platformName", "scopes"],
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
version: { type: "string" },
|
|
130
|
-
},
|
|
131
|
-
required: ["platforms"],
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
const USE_PLATFORM_TOOL = {
|
|
135
|
-
name: "use_platform",
|
|
136
|
-
description: "use_platform: Call an external platform API (Airtable, GitHub, Slack, Metabase, etc.) through the AgentValet proxy.\nInput: platform (string), endpoint (string), method (GET|POST|PUT|PATCH|DELETE), scope (string), body (object, optional — JSON request body for POST/PUT/PATCH/DELETE).\nReturns: upstream API response body. May take up to 50 seconds when the action requires owner approval — the call will block while we wait, then return the approved result transparently. If approval doesn't land in time, returns a `pending_approval` envelope and the action runs asynchronously; the user is notified when it completes.\nAuth: Bearer JWT.\nNote: legacy clients passing `data` instead of `body` are still accepted for backwards compatibility, but `body` is the canonical name.",
|
|
137
|
-
inputSchema: {
|
|
138
|
-
type: "object",
|
|
139
|
-
properties: {
|
|
140
|
-
platform: {
|
|
141
|
-
type: "string",
|
|
142
|
-
description: "Platform ID (e.g. airtable, github, slack, metabase)",
|
|
143
|
-
},
|
|
144
|
-
endpoint: {
|
|
145
|
-
type: "string",
|
|
146
|
-
description: "API path on the target platform (e.g. /v0/meta/bases or /api/dataset)",
|
|
147
|
-
},
|
|
148
|
-
method: {
|
|
149
|
-
type: "string",
|
|
150
|
-
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
151
|
-
description: "HTTP method to use",
|
|
152
|
-
},
|
|
153
|
-
scope: {
|
|
154
|
-
type: "string",
|
|
155
|
-
description: "Permission scope required for this action (e.g. records:read)",
|
|
156
|
-
},
|
|
157
|
-
body: {
|
|
158
|
-
type: "object",
|
|
159
|
-
description: "JSON request body for POST/PUT/PATCH/DELETE. Optional. Forwarded verbatim to the upstream API.",
|
|
160
|
-
},
|
|
161
|
-
data: {
|
|
162
|
-
type: "object",
|
|
163
|
-
description: "Deprecated alias for `body` — prefer `body`. Kept for backwards compatibility.",
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
required: ["platform", "endpoint", "method", "scope"],
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
const AGENT_REGISTER_TOOL = {
|
|
170
|
-
name: "agent_register",
|
|
171
|
-
description: "agent_register: Self-register this agent with an owner. No auth required.\nInput: owner_id (string), agent_name (string), requested_scopes (array of {platformId, scopes}).\nReturns: registration_token, poll_url, client_id, scope, expires_in.\nAuth: None.",
|
|
172
|
-
inputSchema: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
175
|
-
owner_id: {
|
|
176
|
-
type: "string",
|
|
177
|
-
description: "The owner ID to register this agent under",
|
|
178
|
-
},
|
|
179
|
-
agent_name: {
|
|
180
|
-
type: "string",
|
|
181
|
-
description: "Human-readable name for this agent",
|
|
182
|
-
},
|
|
183
|
-
requested_scopes: {
|
|
184
|
-
type: "array",
|
|
185
|
-
description: "Array of platform scope requests",
|
|
186
|
-
items: {
|
|
187
|
-
type: "object",
|
|
188
|
-
properties: {
|
|
189
|
-
platformId: { type: "string" },
|
|
190
|
-
scopes: { type: "array", items: { type: "string" } },
|
|
191
|
-
},
|
|
192
|
-
required: ["platformId", "scopes"],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
required: ["owner_id", "agent_name", "requested_scopes"],
|
|
197
|
-
},
|
|
198
|
-
outputSchema: {
|
|
199
|
-
type: "object",
|
|
200
|
-
properties: {
|
|
201
|
-
registration_token: { type: "string" },
|
|
202
|
-
poll_url: { type: "string" },
|
|
203
|
-
client_id: { type: "string" },
|
|
204
|
-
scope: { type: "string" },
|
|
205
|
-
expires_in: { type: "number" },
|
|
206
|
-
},
|
|
207
|
-
required: ["registration_token"],
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
const AGENT_STATUS_TOOL = {
|
|
211
|
-
name: "agent_status",
|
|
212
|
-
description: "agent_status: Poll registration status using the token from agent_register.\nInput: token (string, required).\nReturns: status (\"pending_approval\"|\"approved\"|\"rejected\"), agent_id (if approved), mcp_config (if approved).\nAuth: None.",
|
|
213
|
-
inputSchema: {
|
|
214
|
-
type: "object",
|
|
215
|
-
properties: {
|
|
216
|
-
token: {
|
|
217
|
-
type: "string",
|
|
218
|
-
description: "Registration token returned by agent_register",
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
required: ["token"],
|
|
222
|
-
},
|
|
223
|
-
outputSchema: {
|
|
224
|
-
type: "object",
|
|
225
|
-
properties: {
|
|
226
|
-
status: { type: "string", enum: ["pending_approval", "approved", "rejected"] },
|
|
227
|
-
agent_id: { type: "string" },
|
|
228
|
-
mcp_config: { type: "object" },
|
|
229
|
-
},
|
|
230
|
-
required: ["status"],
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
const AUTHZEN_EVALUATE_TOOL = {
|
|
234
|
-
name: "authzen_evaluate",
|
|
235
|
-
description: "authzen_evaluate: Evaluate whether this agent has access to a specific platform scope. Call this BEFORE use_platform when you want to pre-check without making the upstream call.\nInput: platform_id (string), scope (string).\nReturns: decision (boolean), reason (\"approved\"|\"denied\"|\"revoked\"|\"scope_not_granted\").\nAuth: Bearer agent JWT (sent automatically by this MCP server).",
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: {
|
|
239
|
-
platform_id: {
|
|
240
|
-
type: "string",
|
|
241
|
-
description: "The platform identifier (e.g. airtable, github)",
|
|
242
|
-
},
|
|
243
|
-
scope: {
|
|
244
|
-
type: "string",
|
|
245
|
-
description: "The permission scope to evaluate (e.g. records:read)",
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
required: ["platform_id", "scope"],
|
|
249
|
-
},
|
|
250
|
-
outputSchema: {
|
|
251
|
-
type: "object",
|
|
252
|
-
properties: {
|
|
253
|
-
decision: { type: "boolean" },
|
|
254
|
-
context: { type: "object", properties: { reason: { type: "string" } } },
|
|
255
|
-
},
|
|
256
|
-
required: ["decision"],
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
const REPORT_SELF_DIAGNOSTIC_TOOL = {
|
|
260
|
-
name: "report_self_diagnostic",
|
|
261
|
-
description: "report_self_diagnostic: Lodge a self-report (error/warning/info) with the AgentValet owner. Use after a use_platform error returns a report_hint, OR proactively when you encounter a problem the user should know about.\nInput: severity (debug|info|warn|error|critical), message (string, required, max 4096 bytes), code (string, optional, max 128 chars), platform (string, optional), endpoint (string, optional), correlation_id (uuid string, optional — copy from the failing call's report_hint to stitch this report to the broker-side audit row), context (object, optional, JSON-serialised must be < 16 KiB).\nReturns: { id, received_at } on success.\nAuth: Bearer agent JWT (sent automatically).",
|
|
262
|
-
inputSchema: {
|
|
263
|
-
type: "object",
|
|
264
|
-
properties: {
|
|
265
|
-
severity: {
|
|
266
|
-
type: "string",
|
|
267
|
-
enum: ["debug", "info", "warn", "error", "critical"],
|
|
268
|
-
description: "Severity level. error/critical trigger an owner notification.",
|
|
269
|
-
},
|
|
270
|
-
message: {
|
|
271
|
-
type: "string",
|
|
272
|
-
description: "One-sentence agent narrative describing what happened.",
|
|
273
|
-
},
|
|
274
|
-
code: {
|
|
275
|
-
type: "string",
|
|
276
|
-
description: "Optional short machine code (e.g. 'permission_denied').",
|
|
277
|
-
},
|
|
278
|
-
platform: {
|
|
279
|
-
type: "string",
|
|
280
|
-
description: "Optional platform id this report relates to.",
|
|
281
|
-
},
|
|
282
|
-
endpoint: {
|
|
283
|
-
type: "string",
|
|
284
|
-
description: "Optional endpoint that failed.",
|
|
285
|
-
},
|
|
286
|
-
correlation_id: {
|
|
287
|
-
type: "string",
|
|
288
|
-
description: "Optional UUID — copy from a use_platform error's report_hint to stitch this report to the audit row.",
|
|
289
|
-
},
|
|
290
|
-
context: {
|
|
291
|
-
type: "object",
|
|
292
|
-
description: "Optional structured context (request params, error details). Avoid secrets.",
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
required: ["severity", "message"],
|
|
296
|
-
},
|
|
297
|
-
outputSchema: {
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: {
|
|
300
|
-
id: { type: "string" },
|
|
301
|
-
received_at: { type: "string" },
|
|
302
|
-
},
|
|
303
|
-
required: ["id"],
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
const LIST_MY_PENDING_ACTIONS_TOOL = {
|
|
307
|
-
name: "list_my_pending_actions",
|
|
308
|
-
description: "list_my_pending_actions: Returns this agent's currently-pending approval requests AND any that completed in the last 24 hours. Use this at session start when the user mentions an earlier action, or when use_platform's long-poll timed out and the user comes back asking what happened.\nInput: None.\nReturns: { pending: [{approval_id, platform_id, scope, created_at, expires_at}], recently_completed: [{approval_id, platform_id, scope, status, executed_at, result_summary, execution_error}] }.\nAuth: Bearer agent JWT (sent automatically).",
|
|
309
|
-
inputSchema: { type: "object", properties: {} },
|
|
310
|
-
outputSchema: {
|
|
311
|
-
type: "object",
|
|
312
|
-
properties: {
|
|
313
|
-
pending: {
|
|
314
|
-
type: "array",
|
|
315
|
-
items: {
|
|
316
|
-
type: "object",
|
|
317
|
-
properties: {
|
|
318
|
-
approval_id: { type: "string" },
|
|
319
|
-
platform_id: { type: "string" },
|
|
320
|
-
scope: { type: "string" },
|
|
321
|
-
created_at: { type: "string" },
|
|
322
|
-
expires_at: { type: "string" },
|
|
323
|
-
},
|
|
324
|
-
required: ["approval_id", "platform_id", "scope"],
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
recently_completed: {
|
|
328
|
-
type: "array",
|
|
329
|
-
items: {
|
|
330
|
-
type: "object",
|
|
331
|
-
properties: {
|
|
332
|
-
approval_id: { type: "string" },
|
|
333
|
-
platform_id: { type: "string" },
|
|
334
|
-
scope: { type: "string" },
|
|
335
|
-
status: { type: "string" },
|
|
336
|
-
executed_at: { type: "string" },
|
|
337
|
-
result_summary: { type: "string" },
|
|
338
|
-
execution_error: { type: "string" },
|
|
339
|
-
},
|
|
340
|
-
required: ["approval_id", "status"],
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
required: ["pending", "recently_completed"],
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
// TODO: intent_resolve tool — planned for future release
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
// Boot-time platform fetch — primes the instruction block with the live
|
|
350
|
-
// platform list so the host LLM sees an exact catalogue. Best-effort: if the
|
|
351
|
-
// fetch fails (agent not activated, network down), we fall back to the static
|
|
352
|
-
// instructions and rely on the agent calling list_platforms at runtime.
|
|
41
|
+
// MCP server setup
|
|
353
42
|
// ---------------------------------------------------------------------------
|
|
354
|
-
|
|
43
|
+
const server = new Server({ name: "agentvalet", version: "1.0.0" }, { capabilities: { tools: {} }, instructions: renderInstructions(undefined) });
|
|
44
|
+
// The config + server bundle threaded into auth + handlers (see context.ts) —
|
|
45
|
+
// keeps those modules free of globals.
|
|
46
|
+
const ctx = { AGENT_ID, OWNER_ID, PROXY_URL, AGENT_PRIVATE_KEY_RAW, privateKey, server };
|
|
47
|
+
// Boot-time platform fetch — primes the proxy connection and surfaces auth
|
|
48
|
+
// failures in the stderr boot diagnostics. Best-effort and fire-and-forget so
|
|
49
|
+
// it can NEVER delay the `initialize` response (a top-level await here used to
|
|
50
|
+
// block Claude Desktop for seconds on cold Azure CA and cause host timeouts).
|
|
51
|
+
// The host LLM learns the catalogue from list_platforms at runtime.
|
|
52
|
+
void (async () => {
|
|
355
53
|
if (AGENT_PRIVATE_KEY_RAW === null)
|
|
356
|
-
return
|
|
54
|
+
return;
|
|
357
55
|
try {
|
|
358
|
-
const token = await signJWT();
|
|
56
|
+
const token = await signJWT(ctx);
|
|
359
57
|
const ac = new AbortController();
|
|
360
58
|
const timer = setTimeout(() => ac.abort(), 4_000);
|
|
361
|
-
|
|
59
|
+
await fetch(`${PROXY_URL}/v1/agent/permissions`, {
|
|
362
60
|
method: "GET",
|
|
363
61
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
364
62
|
signal: ac.signal,
|
|
365
63
|
}).finally(() => clearTimeout(timer));
|
|
366
|
-
if (!res.ok)
|
|
367
|
-
return undefined;
|
|
368
|
-
const body = (await res.json());
|
|
369
|
-
const names = (body.platforms ?? [])
|
|
370
|
-
.map((p) => p.platformName ?? p.platformId)
|
|
371
|
-
.filter((n) => typeof n === "string" && n.length > 0);
|
|
372
|
-
return names.length > 0 ? names : undefined;
|
|
373
64
|
}
|
|
374
65
|
catch {
|
|
375
|
-
|
|
66
|
+
// best-effort warmup only
|
|
376
67
|
}
|
|
377
|
-
}
|
|
378
|
-
// NOTE: We intentionally do NOT prefetch platform names at boot. Doing so
|
|
379
|
-
// added a top-level await on the proxy and blocked the `initialize` response
|
|
380
|
-
// to Claude Desktop for several seconds (worse on cold Azure CA), causing
|
|
381
|
-
// host-side timeouts. The LLM learns the catalogue from list_platforms at
|
|
382
|
-
// runtime — boot-time enrichment was nice-to-have, not load-bearing.
|
|
383
|
-
// ---------------------------------------------------------------------------
|
|
384
|
-
// MCP server setup
|
|
385
|
-
// ---------------------------------------------------------------------------
|
|
386
|
-
const server = new Server({ name: "agentvalet", version: "1.0.0" }, { capabilities: { tools: {} }, instructions: renderInstructions(undefined) });
|
|
387
|
-
// Fire the platform-names fetch in the background after the server is up so
|
|
388
|
-
// it can't delay initialize. The result isn't surfaced to the host (MCP has
|
|
389
|
-
// no instructions-update message), but the network warmup primes the proxy
|
|
390
|
-
// and surfaces auth failures in the stderr boot diagnostics path.
|
|
391
|
-
void fetchPlatformNamesForInstructions().catch(() => { });
|
|
68
|
+
})().catch(() => { });
|
|
392
69
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
393
|
-
tools:
|
|
394
|
-
LIST_PLATFORMS_TOOL,
|
|
395
|
-
USE_PLATFORM_TOOL,
|
|
396
|
-
AGENT_REGISTER_TOOL,
|
|
397
|
-
AGENT_STATUS_TOOL,
|
|
398
|
-
AUTHZEN_EVALUATE_TOOL,
|
|
399
|
-
REPORT_SELF_DIAGNOSTIC_TOOL,
|
|
400
|
-
LIST_MY_PENDING_ACTIONS_TOOL,
|
|
401
|
-
],
|
|
70
|
+
tools: ALL_TOOLS,
|
|
402
71
|
}));
|
|
403
72
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
404
73
|
const { name, arguments: args } = request.params;
|
|
405
74
|
if (name === "list_platforms") {
|
|
406
|
-
return await handleListPlatforms();
|
|
75
|
+
return await handleListPlatforms(ctx);
|
|
407
76
|
}
|
|
408
77
|
if (name === "use_platform") {
|
|
409
78
|
if (!args ||
|
|
@@ -429,40 +98,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
429
98
|
// schema only declared `data` which made every POST get silently
|
|
430
99
|
// dropped to an empty body. See the use_platform tool description.
|
|
431
100
|
const bodyArg = (args.body ?? args.data);
|
|
432
|
-
return await handleUsePlatform({
|
|
101
|
+
return await handleUsePlatform(ctx, {
|
|
433
102
|
platform: args.platform,
|
|
434
103
|
endpoint: args.endpoint,
|
|
435
104
|
method: args.method,
|
|
436
105
|
scope: args.scope,
|
|
437
106
|
data: bodyArg,
|
|
107
|
+
...(typeof args.connection_id === "string" ? { connection_id: args.connection_id } : {}),
|
|
438
108
|
}, progressToken);
|
|
439
109
|
}
|
|
440
110
|
if (name === "agent_register") {
|
|
441
111
|
if (!args || typeof args.owner_id !== "string" || typeof args.agent_name !== "string" || !Array.isArray(args.requested_scopes)) {
|
|
442
112
|
return errorContent("Invalid or missing arguments: owner_id, agent_name, requested_scopes are required");
|
|
443
113
|
}
|
|
444
|
-
return await handleAgentRegister(args);
|
|
114
|
+
return await handleAgentRegister(ctx, args);
|
|
445
115
|
}
|
|
446
116
|
if (name === "agent_status") {
|
|
447
117
|
if (!args || typeof args.token !== "string") {
|
|
448
118
|
return errorContent("Invalid or missing argument: token is required");
|
|
449
119
|
}
|
|
450
|
-
return await handleAgentStatus(args.token);
|
|
120
|
+
return await handleAgentStatus(ctx, args.token);
|
|
451
121
|
}
|
|
452
122
|
if (name === "authzen_evaluate") {
|
|
453
123
|
if (!args || typeof args.platform_id !== "string" || typeof args.scope !== "string") {
|
|
454
124
|
return errorContent("Invalid or missing arguments: platform_id and scope are required");
|
|
455
125
|
}
|
|
456
|
-
return await handleAuthzenEvaluate(args.platform_id, args.scope);
|
|
126
|
+
return await handleAuthzenEvaluate(ctx, args.platform_id, args.scope);
|
|
457
127
|
}
|
|
458
128
|
if (name === "list_my_pending_actions") {
|
|
459
|
-
return await handleListMyPendingActions();
|
|
129
|
+
return await handleListMyPendingActions(ctx);
|
|
460
130
|
}
|
|
461
131
|
if (name === "report_self_diagnostic") {
|
|
462
132
|
if (!args || typeof args.severity !== "string" || typeof args.message !== "string") {
|
|
463
133
|
return errorContent("Invalid or missing arguments: severity and message are required");
|
|
464
134
|
}
|
|
465
|
-
return await handleReportSelfDiagnostic(args);
|
|
135
|
+
return await handleReportSelfDiagnostic(ctx, args);
|
|
466
136
|
}
|
|
467
137
|
return {
|
|
468
138
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
@@ -470,365 +140,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
470
140
|
};
|
|
471
141
|
});
|
|
472
142
|
// ---------------------------------------------------------------------------
|
|
473
|
-
// Helpers
|
|
474
|
-
// ---------------------------------------------------------------------------
|
|
475
|
-
function fetchWithTimeout(url, init, timeoutMs = 15_000) {
|
|
476
|
-
const ac = new AbortController();
|
|
477
|
-
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
478
|
-
return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
|
|
479
|
-
}
|
|
480
|
-
async function fetchWithAuth(url, init) {
|
|
481
|
-
const makeRequest = async () => {
|
|
482
|
-
const token = await signJWT();
|
|
483
|
-
return fetchWithTimeout(url, {
|
|
484
|
-
...init,
|
|
485
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init.headers },
|
|
486
|
-
});
|
|
487
|
-
};
|
|
488
|
-
const response = await makeRequest();
|
|
489
|
-
// Retry once on 401 — the JWT may have been issued just before clock skew threshold
|
|
490
|
-
if (response.status === 401) {
|
|
491
|
-
return makeRequest();
|
|
492
|
-
}
|
|
493
|
-
return response;
|
|
494
|
-
}
|
|
495
|
-
function errorContent(message) {
|
|
496
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
497
|
-
}
|
|
498
|
-
// Return helper that gives an MCP-aware host the structured content directly
|
|
499
|
-
// (no parse step), while still emitting the text-content envelope every MCP
|
|
500
|
-
// client understands. Avoids the "list of length 1 with empty fields → let me
|
|
501
|
-
// parse the wrapper" round-trip Claude does on tool results that are raw
|
|
502
|
-
// JSON strings — every read-heavy use_platform call pays that cost otherwise.
|
|
503
|
-
//
|
|
504
|
-
// If `body` isn't valid JSON, we just return the text envelope unchanged —
|
|
505
|
-
// callers that emit prose (summaries, error strings) get the old behaviour.
|
|
506
|
-
function jsonContent(body) {
|
|
507
|
-
const parsed = tryParseJson(body);
|
|
508
|
-
if (parsed === undefined) {
|
|
509
|
-
return { content: [{ type: "text", text: body }] };
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
content: [{ type: "text", text: body }],
|
|
513
|
-
structuredContent: parsed,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
function tryParseJson(text) {
|
|
517
|
-
const trimmed = text.trim();
|
|
518
|
-
// Only attempt parse if it looks structured. Avoids parsing a stray "true"
|
|
519
|
-
// / number / etc. into structuredContent and confusing the host.
|
|
520
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
521
|
-
return undefined;
|
|
522
|
-
try {
|
|
523
|
-
const v = JSON.parse(trimmed);
|
|
524
|
-
if (v && typeof v === "object")
|
|
525
|
-
return v;
|
|
526
|
-
return undefined;
|
|
527
|
-
}
|
|
528
|
-
catch {
|
|
529
|
-
return undefined;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
// Tool handlers
|
|
534
|
-
// ---------------------------------------------------------------------------
|
|
535
|
-
async function handleListPlatforms() {
|
|
536
|
-
const gate = await requireCredentials();
|
|
537
|
-
if (gate)
|
|
538
|
-
return gate;
|
|
539
|
-
let response;
|
|
540
|
-
try {
|
|
541
|
-
response = await fetchWithAuth(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
return errorContent(diagnoseNetworkError(err, PROXY_URL));
|
|
545
|
-
}
|
|
546
|
-
const body = await response.text();
|
|
547
|
-
if (!response.ok)
|
|
548
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
549
|
-
// Proxy wraps the payload as { data: { platforms, version }, _meta: {...} }
|
|
550
|
-
// but outputSchema declares the flat { platforms, version } shape, so the
|
|
551
|
-
// wrapped envelope fails strict structuredContent validation and the host
|
|
552
|
-
// surfaces "tool execution failed (no upstream body)". Unwrap .data.
|
|
553
|
-
const parsed = tryParseJson(body);
|
|
554
|
-
const inner = parsed && typeof parsed === "object" && parsed !== null && "data" in parsed
|
|
555
|
-
? parsed.data
|
|
556
|
-
: parsed;
|
|
557
|
-
if (inner && typeof inner === "object") {
|
|
558
|
-
return {
|
|
559
|
-
content: [{ type: "text", text: body }],
|
|
560
|
-
structuredContent: inner,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
return jsonContent(body);
|
|
564
|
-
}
|
|
565
|
-
// Translates a fetch() failure into something an end-user can actually act on.
|
|
566
|
-
// The default "Network error: fetch failed" message tells a non-developer
|
|
567
|
-
// nothing. Look at the underlying cause keyword and map to a concrete fix.
|
|
568
|
-
function diagnoseNetworkError(err, proxyUrl) {
|
|
569
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
570
|
-
const lower = raw.toLowerCase();
|
|
571
|
-
// Node's undici surfaces DNS failures as "getaddrinfo ENOTFOUND <host>".
|
|
572
|
-
if (lower.includes("enotfound") || lower.includes("getaddrinfo")) {
|
|
573
|
-
return `Network error: cannot resolve ${proxyUrl}. Check your DNS / corporate proxy / VPN, or confirm the PROXY_URL setting is correct. Raw: ${raw}`;
|
|
574
|
-
}
|
|
575
|
-
// Connection refused / unreachable / TLS handshake failure.
|
|
576
|
-
if (lower.includes("econnrefused") || lower.includes("econnreset")) {
|
|
577
|
-
return `Network error: connection to ${proxyUrl} was refused or reset. The proxy may be down — check https://status.agentvalet.ai — or a firewall is blocking the request. Raw: ${raw}`;
|
|
578
|
-
}
|
|
579
|
-
if (lower.includes("etimedout") || lower.includes("timeout") || lower.includes("aborterror")) {
|
|
580
|
-
return `Network error: request to ${proxyUrl} timed out. Likely causes: VPN routing, corporate proxy buffering, or slow network. Try again or confirm api.agentvalet.ai is reachable from this machine. Raw: ${raw}`;
|
|
581
|
-
}
|
|
582
|
-
if (lower.includes("self signed") || lower.includes("cert") || lower.includes("ssl") || lower.includes("tls")) {
|
|
583
|
-
return `Network error: TLS / certificate problem talking to ${proxyUrl}. A corporate MITM proxy may be intercepting traffic. Raw: ${raw}`;
|
|
584
|
-
}
|
|
585
|
-
return `Network error reaching ${proxyUrl}: ${raw}. Check VPN, corporate proxy, and firewall rules for api.agentvalet.ai. If the proxy itself is down, see https://status.agentvalet.ai.`;
|
|
586
|
-
}
|
|
587
|
-
// Long-poll budget — under Claude Desktop's hardcoded 60s tool timeout
|
|
588
|
-
// (with 10s of safety). After this we return a graceful "queued" message and
|
|
589
|
-
// the action lands in the user's pending-actions list (Layer 4).
|
|
590
|
-
const APPROVAL_POLL_BUDGET_MS = 50_000;
|
|
591
|
-
const APPROVAL_POLL_INTERVAL_MS = 2_000;
|
|
592
|
-
const APPROVAL_PROGRESS_INTERVAL_MS = 5_000;
|
|
593
|
-
async function handleUsePlatform(params, progressToken) {
|
|
594
|
-
const gate = await requireCredentials();
|
|
595
|
-
if (gate)
|
|
596
|
-
return gate;
|
|
597
|
-
const requestBody = {
|
|
598
|
-
platform: params.platform,
|
|
599
|
-
endpoint: params.endpoint,
|
|
600
|
-
method: params.method,
|
|
601
|
-
scope: params.scope,
|
|
602
|
-
...(params.data !== undefined && { data: params.data }),
|
|
603
|
-
};
|
|
604
|
-
let response;
|
|
605
|
-
try {
|
|
606
|
-
response = await fetchWithAuth(`${PROXY_URL}/v1/actions`, {
|
|
607
|
-
method: "POST",
|
|
608
|
-
body: JSON.stringify(requestBody),
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
catch (err) {
|
|
612
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
613
|
-
}
|
|
614
|
-
const body = await response.text();
|
|
615
|
-
// 202 + approval_id → enter long-poll. The proxy returns this when the
|
|
616
|
-
// owner needs to approve the call. We poll /v1/approvals/:id every ~2s
|
|
617
|
-
// for up to 50s; if the owner approves in time the proxy re-runs the
|
|
618
|
-
// upstream call and we return its result to the agent transparently.
|
|
619
|
-
if (response.status === 202) {
|
|
620
|
-
const parsed = safeJsonParse(body);
|
|
621
|
-
const approvalId = parsed?.approval_id;
|
|
622
|
-
if (approvalId) {
|
|
623
|
-
return await waitForApproval(approvalId, params, progressToken);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if (!response.ok)
|
|
627
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
628
|
-
return jsonContent(body);
|
|
629
|
-
}
|
|
630
|
-
async function waitForApproval(approvalId, originalCall, progressToken) {
|
|
631
|
-
const startedAt = Date.now();
|
|
632
|
-
let lastProgressAt = 0;
|
|
633
|
-
// Initial progress so the user sees activity immediately.
|
|
634
|
-
await sendProgress(progressToken, 0, APPROVAL_POLL_BUDGET_MS, `Owner approval required for ${originalCall.platform}:${originalCall.scope} — waiting…`);
|
|
635
|
-
while (Date.now() - startedAt < APPROVAL_POLL_BUDGET_MS) {
|
|
636
|
-
await sleep(APPROVAL_POLL_INTERVAL_MS);
|
|
637
|
-
const elapsed = Date.now() - startedAt;
|
|
638
|
-
let res;
|
|
639
|
-
try {
|
|
640
|
-
res = await fetchWithAuth(`${PROXY_URL}/v1/approvals/${approvalId}`, { method: "GET" });
|
|
641
|
-
}
|
|
642
|
-
catch {
|
|
643
|
-
// Transient network error — try again next tick.
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
if (!res.ok) {
|
|
647
|
-
// 404/410 etc. — treat as terminal. Surface back to the agent.
|
|
648
|
-
const text = await res.text();
|
|
649
|
-
return errorContent(`Approval status error ${res.status}: ${text}`);
|
|
650
|
-
}
|
|
651
|
-
const status = (await res.json());
|
|
652
|
-
if (status.status === "approved" && status.executed_at && status.result) {
|
|
653
|
-
// Re-execution complete. Return the upstream response transparently —
|
|
654
|
-
// the agent sees this as if the original use_platform call just succeeded.
|
|
655
|
-
const r = status.result;
|
|
656
|
-
// Mirror the non-2xx-from-upstream behaviour of the regular path.
|
|
657
|
-
if (r.status < 200 || r.status >= 300) {
|
|
658
|
-
return errorContent(`Upstream returned ${r.status}: ${JSON.stringify(r.data)}`);
|
|
659
|
-
}
|
|
660
|
-
// Two-channel return: the text envelope carries the human-readable
|
|
661
|
-
// "[Owner approved after Ns]" prefix (visible to non-structured-aware
|
|
662
|
-
// clients), and structuredContent carries the parsed upstream JSON
|
|
663
|
-
// directly so MCP-aware hosts (Claude Desktop ≥ MCP spec 2025-06-18)
|
|
664
|
-
// never have to strip-the-prefix-and-reparse. Without the second
|
|
665
|
-
// channel the prefix forces Claude into the same wrapper-parse
|
|
666
|
-
// round-trip the bare /v1/actions path used to.
|
|
667
|
-
const data = r.data ?? null;
|
|
668
|
-
const summary = `[Owner approved after ${Math.round(elapsed / 1000)}s]\n` +
|
|
669
|
-
JSON.stringify(data);
|
|
670
|
-
const result = {
|
|
671
|
-
content: [{ type: "text", text: summary }],
|
|
672
|
-
};
|
|
673
|
-
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
674
|
-
result.structuredContent = data;
|
|
675
|
-
}
|
|
676
|
-
return result;
|
|
677
|
-
}
|
|
678
|
-
if (status.status === "denied") {
|
|
679
|
-
return errorContent(`Owner denied this action${status.execution_error ? `: ${status.execution_error}` : "."}`);
|
|
680
|
-
}
|
|
681
|
-
if (status.status === "expired") {
|
|
682
|
-
return errorContent("Approval request expired before the owner responded.");
|
|
683
|
-
}
|
|
684
|
-
if (status.execution_error) {
|
|
685
|
-
// Approved but re-execution failed (network / upstream / agent revoked).
|
|
686
|
-
return errorContent(`Approved but re-execution failed: ${status.execution_error}`);
|
|
687
|
-
}
|
|
688
|
-
// Still pending — emit a progress update at most once per ~5s so the
|
|
689
|
-
// user can see the wait advancing in the Claude Desktop tool UI.
|
|
690
|
-
if (Date.now() - lastProgressAt >= APPROVAL_PROGRESS_INTERVAL_MS) {
|
|
691
|
-
await sendProgress(progressToken, elapsed, APPROVAL_POLL_BUDGET_MS, `Waiting for owner approval — ${Math.round(elapsed / 1000)}s elapsed`);
|
|
692
|
-
lastProgressAt = Date.now();
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// Timed out — fall through to async-recap path. The action is still
|
|
696
|
-
// queued and will run when the owner approves; the user is notified via
|
|
697
|
-
// push/email at that point. Layer 4's list_my_pending_actions surfaces it.
|
|
698
|
-
return jsonContent(JSON.stringify({
|
|
699
|
-
status: "pending_approval",
|
|
700
|
-
approval_id: approvalId,
|
|
701
|
-
message: `Owner hasn't approved within ${APPROVAL_POLL_BUDGET_MS / 1000}s. ` +
|
|
702
|
-
`The action is queued — your owner will be notified, and you'll see it ` +
|
|
703
|
-
`complete next time we chat (or you can ask me to check pending actions).`,
|
|
704
|
-
}));
|
|
705
|
-
}
|
|
706
|
-
function safeJsonParse(text) {
|
|
707
|
-
try {
|
|
708
|
-
return JSON.parse(text);
|
|
709
|
-
}
|
|
710
|
-
catch {
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function sleep(ms) {
|
|
715
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
716
|
-
}
|
|
717
|
-
async function sendProgress(token, progress, total, message) {
|
|
718
|
-
if (token === undefined)
|
|
719
|
-
return;
|
|
720
|
-
try {
|
|
721
|
-
await server.notification({
|
|
722
|
-
method: "notifications/progress",
|
|
723
|
-
params: { progressToken: token, progress, total, message },
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
catch {
|
|
727
|
-
// Best-effort — never let a failed notification break the call.
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
async function handleAgentRegister(args) {
|
|
731
|
-
let response;
|
|
732
|
-
try {
|
|
733
|
-
response = await fetchWithTimeout(`${PROXY_URL}/v1/register`, {
|
|
734
|
-
method: "POST",
|
|
735
|
-
headers: { "Content-Type": "application/json" },
|
|
736
|
-
body: JSON.stringify(args),
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
catch (err) {
|
|
740
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
741
|
-
}
|
|
742
|
-
const body = await response.text();
|
|
743
|
-
if (!response.ok)
|
|
744
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
745
|
-
return jsonContent(body);
|
|
746
|
-
}
|
|
747
|
-
async function handleAgentStatus(token) {
|
|
748
|
-
let response;
|
|
749
|
-
try {
|
|
750
|
-
response = await fetchWithTimeout(`${PROXY_URL}/v1/register/status/${encodeURIComponent(token)}`, {
|
|
751
|
-
method: "GET",
|
|
752
|
-
headers: { "Content-Type": "application/json" },
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
catch (err) {
|
|
756
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
757
|
-
}
|
|
758
|
-
const body = await response.text();
|
|
759
|
-
if (!response.ok)
|
|
760
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
761
|
-
return jsonContent(body);
|
|
762
|
-
}
|
|
763
|
-
async function handleAuthzenEvaluate(platformId, scope) {
|
|
764
|
-
const gate = await requireCredentials();
|
|
765
|
-
if (gate)
|
|
766
|
-
return gate;
|
|
767
|
-
const authzenBody = {
|
|
768
|
-
subject: { type: "agent", id: AGENT_ID },
|
|
769
|
-
action: { name: "tool_call" },
|
|
770
|
-
resource: { type: "platform_scope", id: `${platformId}:${scope}` },
|
|
771
|
-
};
|
|
772
|
-
let response;
|
|
773
|
-
try {
|
|
774
|
-
response = await fetchWithAuth(`${PROXY_URL}/v1/authzen/access`, {
|
|
775
|
-
method: "POST",
|
|
776
|
-
body: JSON.stringify(authzenBody),
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
catch (err) {
|
|
780
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
781
|
-
}
|
|
782
|
-
const body = await response.text();
|
|
783
|
-
if (!response.ok)
|
|
784
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
785
|
-
return jsonContent(body);
|
|
786
|
-
}
|
|
787
|
-
async function handleListMyPendingActions() {
|
|
788
|
-
const gate = await requireCredentials();
|
|
789
|
-
if (gate)
|
|
790
|
-
return gate;
|
|
791
|
-
let response;
|
|
792
|
-
try {
|
|
793
|
-
response = await fetchWithAuth(`${PROXY_URL}/v1/agents/me/pending-actions`, { method: "GET" });
|
|
794
|
-
}
|
|
795
|
-
catch (err) {
|
|
796
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
797
|
-
}
|
|
798
|
-
const text = await response.text();
|
|
799
|
-
if (!response.ok)
|
|
800
|
-
return errorContent(`Proxy error ${response.status}: ${text}`);
|
|
801
|
-
return jsonContent(text);
|
|
802
|
-
}
|
|
803
|
-
async function handleReportSelfDiagnostic(args) {
|
|
804
|
-
const gate = await requireCredentials();
|
|
805
|
-
if (gate)
|
|
806
|
-
return gate;
|
|
807
|
-
// Whitelist body fields — never forward owner_id/agent_id (proxy derives those from JWT).
|
|
808
|
-
const body = {
|
|
809
|
-
severity: args.severity,
|
|
810
|
-
message: args.message,
|
|
811
|
-
};
|
|
812
|
-
for (const k of ["code", "platform", "endpoint", "correlation_id", "context"]) {
|
|
813
|
-
if (args[k] !== undefined)
|
|
814
|
-
body[k] = args[k];
|
|
815
|
-
}
|
|
816
|
-
let response;
|
|
817
|
-
try {
|
|
818
|
-
response = await fetchWithAuth(`${PROXY_URL}/v1/agents/self/diagnostics`, {
|
|
819
|
-
method: "POST",
|
|
820
|
-
body: JSON.stringify(body),
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
catch (err) {
|
|
824
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
825
|
-
}
|
|
826
|
-
const text = await response.text();
|
|
827
|
-
if (!response.ok)
|
|
828
|
-
return errorContent(`Proxy error ${response.status}: ${text}`);
|
|
829
|
-
return jsonContent(text);
|
|
830
|
-
}
|
|
831
|
-
// ---------------------------------------------------------------------------
|
|
832
143
|
// Connect transport
|
|
833
144
|
// ---------------------------------------------------------------------------
|
|
834
145
|
if (process.env.MCP_TRANSPORT === "http") {
|