@agentvalet/mcp-server 1.3.0 → 1.4.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/gate-client.js +159 -0
- package/dist/index.js +7 -1
- package/dist/tools/handlers.js +62 -2
- package/dist/tools/schemas.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// apps/mcp-server/src/gate-client.ts
|
|
2
|
+
//
|
|
3
|
+
// A thin, dependency-free client over AgentValet's public governance contract.
|
|
4
|
+
// It does NOT implement policy, brokering, or audit. Those live in the proxy.
|
|
5
|
+
// It only does two things:
|
|
6
|
+
//
|
|
7
|
+
// 1. Send a request to the public gate (POST /v1/actions) or the Observe
|
|
8
|
+
// relay (POST /v1/observe/actions).
|
|
9
|
+
// 2. Map the wire response to exactly one of the six documented decisions, so
|
|
10
|
+
// a connector switches on a single discriminated union instead of
|
|
11
|
+
// re-deriving status-code semantics in every integration.
|
|
12
|
+
//
|
|
13
|
+
// The full wire contract these mappings depend on is documented in
|
|
14
|
+
// docs/contract/public-governance-gate.md. Keep this file and that doc in step.
|
|
15
|
+
//
|
|
16
|
+
// fetch is injectable (fetchImpl) so the mapping is testable without a network;
|
|
17
|
+
// it defaults to the global fetch.
|
|
18
|
+
/** Agent states that mean "the agent itself is halted", surfaced as a 403 by the
|
|
19
|
+
* permission check. Distinct from a policy denial of an otherwise-healthy agent. */
|
|
20
|
+
const SUSPENDED_REASONS = new Set(["agent_suspended", "circuit_breaker_open"]);
|
|
21
|
+
function readField(body, key) {
|
|
22
|
+
if (body && typeof body === "object" && key in body) {
|
|
23
|
+
const v = body[key];
|
|
24
|
+
return typeof v === "string" ? v : undefined;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pure mapper: a public-gate response (HTTP status + parsed body) to a decision.
|
|
30
|
+
* Exported so connectors and tests can classify a response they already hold.
|
|
31
|
+
*
|
|
32
|
+
* Mapping (POST /v1/actions):
|
|
33
|
+
* 2xx -> allowed
|
|
34
|
+
* 202 -> pending_approval
|
|
35
|
+
* 403 reason in {agent_suspended,
|
|
36
|
+
* circuit_breaker_open} -> suspended
|
|
37
|
+
* 401 / 5xx -> error
|
|
38
|
+
* other 4xx (402/403/412/429/...) -> denied
|
|
39
|
+
*/
|
|
40
|
+
export function classifyGateResponse(status, body) {
|
|
41
|
+
if (status === 202) {
|
|
42
|
+
return {
|
|
43
|
+
decision: "pending_approval",
|
|
44
|
+
status,
|
|
45
|
+
approvalId: readField(body, "approval_id") ?? null,
|
|
46
|
+
message: readField(body, "message"),
|
|
47
|
+
raw: body,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (status >= 200 && status < 300) {
|
|
51
|
+
return { decision: "allowed", status, data: body };
|
|
52
|
+
}
|
|
53
|
+
if (status === 403) {
|
|
54
|
+
const reason = readField(body, "reason");
|
|
55
|
+
if (reason && SUSPENDED_REASONS.has(reason)) {
|
|
56
|
+
return { decision: "suspended", status, reason, raw: body };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Identity rejection and upstream/server failures are not policy decisions.
|
|
60
|
+
if (status === 401 || status >= 500) {
|
|
61
|
+
return {
|
|
62
|
+
decision: "error",
|
|
63
|
+
status,
|
|
64
|
+
code: readField(body, "code") ?? (status === 401 ? "unauthorized" : null),
|
|
65
|
+
correlationId: readField(body, "correlation_id"),
|
|
66
|
+
raw: body,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Every other 4xx (402 billing, 403 policy, 412 not-connected, 429 rate) is a
|
|
70
|
+
// governance refusal of an otherwise-healthy agent.
|
|
71
|
+
return {
|
|
72
|
+
decision: "denied",
|
|
73
|
+
status,
|
|
74
|
+
reason: readField(body, "reason") ?? readField(body, "error") ?? null,
|
|
75
|
+
correlationId: readField(body, "correlation_id"),
|
|
76
|
+
raw: body,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function parseBody(res) {
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(text);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Call the public governance gate (POST /v1/actions) and return a classified
|
|
90
|
+
* outcome. Never throws for a gate response or a transport failure: a transport
|
|
91
|
+
* failure resolves to a GateError with code "network_error" so callers handle
|
|
92
|
+
* every path through the same switch.
|
|
93
|
+
*/
|
|
94
|
+
export async function callGate(client, req) {
|
|
95
|
+
const doFetch = client.fetchImpl ?? fetch;
|
|
96
|
+
const requestBody = {
|
|
97
|
+
platform: req.platform,
|
|
98
|
+
endpoint: req.endpoint,
|
|
99
|
+
method: req.method,
|
|
100
|
+
scope: req.scope,
|
|
101
|
+
...(req.data !== undefined ? { data: req.data } : {}),
|
|
102
|
+
...(req.connection_id ? { connection_id: req.connection_id } : {}),
|
|
103
|
+
};
|
|
104
|
+
let res;
|
|
105
|
+
try {
|
|
106
|
+
res = await doFetch(`${client.proxyUrl}/v1/actions`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { Authorization: `Bearer ${client.token}`, "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify(requestBody),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
decision: "error",
|
|
115
|
+
status: 0,
|
|
116
|
+
code: "network_error",
|
|
117
|
+
raw: { error: err instanceof Error ? err.message : String(err) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return classifyGateResponse(res.status, await parseBody(res));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Call the Observe relay (POST /v1/observe/actions). The relay is audit-only and
|
|
124
|
+
* passes the upstream status through, so a completed relay always resolves to
|
|
125
|
+
* GateObserved regardless of the upstream status. The BYO credential rides as a
|
|
126
|
+
* header and never enters the request body. A transport failure resolves to a
|
|
127
|
+
* GateError with code "network_error".
|
|
128
|
+
*/
|
|
129
|
+
export async function observe(client, req) {
|
|
130
|
+
const doFetch = client.fetchImpl ?? fetch;
|
|
131
|
+
const relayBody = {
|
|
132
|
+
platform: req.platform,
|
|
133
|
+
endpoint: req.endpoint,
|
|
134
|
+
method: req.method,
|
|
135
|
+
...(req.action !== undefined ? { action: req.action } : {}),
|
|
136
|
+
...(req.body !== undefined ? { body: req.body } : {}),
|
|
137
|
+
};
|
|
138
|
+
let res;
|
|
139
|
+
try {
|
|
140
|
+
res = await doFetch(`${client.proxyUrl}/v1/observe/actions`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${client.token}`,
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
"X-AV-Observe-Credential": req.credential,
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify(relayBody),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return {
|
|
152
|
+
decision: "error",
|
|
153
|
+
status: 0,
|
|
154
|
+
code: "network_error",
|
|
155
|
+
raw: { error: err instanceof Error ? err.message : String(err) },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { decision: "observed", status: res.status, data: await parseBody(res) };
|
|
159
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { validateConfig } from "./config.js";
|
|
|
22
22
|
import { renderInstructions } from "./instructions.js";
|
|
23
23
|
import { signJWT } from "./auth.js";
|
|
24
24
|
import { ALLOWED_METHODS, ALL_TOOLS } from "./tools/schemas.js";
|
|
25
|
-
import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, } from "./tools/handlers.js";
|
|
25
|
+
import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, handleRequestPlatformAccess, } from "./tools/handlers.js";
|
|
26
26
|
import { errorContent } from "./net.js";
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
// Startup env validation
|
|
@@ -143,6 +143,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
143
143
|
}
|
|
144
144
|
return await handleReportSelfDiagnostic(ctx, args);
|
|
145
145
|
}
|
|
146
|
+
if (name === "request_platform_access") {
|
|
147
|
+
if (!args || typeof args.platform !== "string") {
|
|
148
|
+
return errorContent("Invalid or missing argument: platform is required");
|
|
149
|
+
}
|
|
150
|
+
return await handleRequestPlatformAccess(ctx, args);
|
|
151
|
+
}
|
|
146
152
|
return {
|
|
147
153
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
148
154
|
isError: true,
|
package/dist/tools/handlers.js
CHANGED
|
@@ -114,8 +114,15 @@ export async function handleUsePlatform(ctx, params, progressToken) {
|
|
|
114
114
|
return await waitForApproval(ctx, approvalId, params, progressToken);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
if (!response.ok)
|
|
118
|
-
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
// 403 is the access-denied / scope-not-granted code returned by the
|
|
119
|
+
// proxy's authorize pipeline (see apps/proxy/src/pipeline/authorize.ts).
|
|
120
|
+
// Point the agent at request_platform_access instead of dead-ending it.
|
|
121
|
+
const hint = response.status === 403
|
|
122
|
+
? " You can call request_platform_access({ platform, scope, reason }) to ask an org admin to grant this."
|
|
123
|
+
: "";
|
|
124
|
+
return errorContent(`Proxy error ${response.status}: ${body}${hint}`);
|
|
125
|
+
}
|
|
119
126
|
return jsonContent(body);
|
|
120
127
|
}
|
|
121
128
|
async function waitForApproval(ctx, approvalId, originalCall, progressToken) {
|
|
@@ -280,6 +287,59 @@ export async function handleListMyPendingActions(ctx) {
|
|
|
280
287
|
return errorContent(`Proxy error ${response.status}: ${text}`);
|
|
281
288
|
return jsonContent(text);
|
|
282
289
|
}
|
|
290
|
+
export async function handleRequestPlatformAccess(ctx, args) {
|
|
291
|
+
const gate = await requireCredentials(ctx);
|
|
292
|
+
if (gate)
|
|
293
|
+
return gate;
|
|
294
|
+
const platform = typeof args.platform === "string" ? args.platform : "";
|
|
295
|
+
if (!platform)
|
|
296
|
+
return errorContent("platform is required");
|
|
297
|
+
const scope = typeof args.scope === "string" ? args.scope : undefined;
|
|
298
|
+
const reason = typeof args.reason === "string" ? args.reason : undefined;
|
|
299
|
+
let submit;
|
|
300
|
+
try {
|
|
301
|
+
submit = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/access-request`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
body: JSON.stringify({ platform, scope, reason }),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return errorContent(diagnoseNetworkError(err, ctx.PROXY_URL));
|
|
308
|
+
}
|
|
309
|
+
if (!submit.ok && submit.status !== 202 && submit.status !== 200) {
|
|
310
|
+
const t = await submit.text();
|
|
311
|
+
return errorContent(`Access request failed (${submit.status}): ${t}`);
|
|
312
|
+
}
|
|
313
|
+
const submitted = (await submit.json());
|
|
314
|
+
const token = submitted.request_token;
|
|
315
|
+
if (!token)
|
|
316
|
+
return errorContent("Access request did not return a token.");
|
|
317
|
+
// Poll the request status for ~50s (2s interval), matching use_platform UX.
|
|
318
|
+
const deadline = Date.now() + 50_000;
|
|
319
|
+
while (Date.now() < deadline) {
|
|
320
|
+
await sleep(2000);
|
|
321
|
+
let statusRes;
|
|
322
|
+
try {
|
|
323
|
+
statusRes = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/access-request/status/${encodeURIComponent(token)}`, { method: "GET" });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!statusRes.ok)
|
|
329
|
+
continue;
|
|
330
|
+
const s = (await statusRes.json());
|
|
331
|
+
if (s.status === "approved") {
|
|
332
|
+
return jsonContent(JSON.stringify({ status: "approved", message: "Access granted — retry your original use_platform call." }));
|
|
333
|
+
}
|
|
334
|
+
if (s.status === "denied") {
|
|
335
|
+
return jsonContent(JSON.stringify({ status: "denied", message: "An admin denied this access request." }));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return jsonContent(JSON.stringify({
|
|
339
|
+
status: "pending",
|
|
340
|
+
message: "Request submitted and awaiting admin approval. Retry your call later.",
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
283
343
|
export async function handleReportSelfDiagnostic(ctx, args) {
|
|
284
344
|
const gate = await requireCredentials(ctx);
|
|
285
345
|
if (gate)
|
package/dist/tools/schemas.js
CHANGED
|
@@ -261,6 +261,27 @@ export const LIST_MY_PENDING_ACTIONS_TOOL = {
|
|
|
261
261
|
required: ["pending", "recently_completed"],
|
|
262
262
|
},
|
|
263
263
|
};
|
|
264
|
+
export const REQUEST_PLATFORM_ACCESS_TOOL = {
|
|
265
|
+
name: "request_platform_access",
|
|
266
|
+
description: "request_platform_access: Ask an org admin to grant this agent access to a platform it is currently blocked from. Call this when use_platform returns an access-denied error. Input: platform (string, required), scope (string, optional — the specific scope you need), reason (string, optional — why you need it). The call waits up to ~50s for an admin decision. Returns: { status: \"approved\"|\"pending\"|\"denied\", message }. On \"approved\", retry your original use_platform call. On \"pending\", the request is queued; retry later. Auth: Bearer agent JWT (sent automatically).",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
platform: { type: "string", description: "Platform ID (e.g. github, slack)" },
|
|
271
|
+
scope: { type: "string", description: "Optional specific scope needed (e.g. repo:read)" },
|
|
272
|
+
reason: { type: "string", description: "Optional short reason for the request" },
|
|
273
|
+
},
|
|
274
|
+
required: ["platform"],
|
|
275
|
+
},
|
|
276
|
+
outputSchema: {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
status: { type: "string", enum: ["approved", "pending", "denied"] },
|
|
280
|
+
message: { type: "string" },
|
|
281
|
+
},
|
|
282
|
+
required: ["status"],
|
|
283
|
+
},
|
|
284
|
+
};
|
|
264
285
|
export const ALL_TOOLS = [
|
|
265
286
|
LIST_PLATFORMS_TOOL,
|
|
266
287
|
USE_PLATFORM_TOOL,
|
|
@@ -269,4 +290,5 @@ export const ALL_TOOLS = [
|
|
|
269
290
|
AUTHZEN_EVALUATE_TOOL,
|
|
270
291
|
REPORT_SELF_DIAGNOSTIC_TOOL,
|
|
271
292
|
LIST_MY_PENDING_ACTIONS_TOOL,
|
|
293
|
+
REQUEST_PLATFORM_ACCESS_TOOL,
|
|
272
294
|
];
|