@agentvalet/mcp-server 1.1.1 → 1.3.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/net.js ADDED
@@ -0,0 +1,80 @@
1
+ // apps/mcp-server/src/net.ts
2
+ //
3
+ // Stateless HTTP + MCP-content helpers. No agent identity / config state — see
4
+ // auth.ts for the credentialed wrappers. Extracted verbatim from index.ts
5
+ // during the modularization (Critique-Roadmap prompt 06.4); behavior unchanged.
6
+ export function fetchWithTimeout(url, init, timeoutMs = 15_000) {
7
+ const ac = new AbortController();
8
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
9
+ return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
10
+ }
11
+ export function errorContent(message) {
12
+ return { content: [{ type: "text", text: message }], isError: true };
13
+ }
14
+ // Return helper that gives an MCP-aware host the structured content directly
15
+ // (no parse step), while still emitting the text-content envelope every MCP
16
+ // client understands. Avoids the "list of length 1 with empty fields → let me
17
+ // parse the wrapper" round-trip Claude does on tool results that are raw
18
+ // JSON strings — every read-heavy use_platform call pays that cost otherwise.
19
+ //
20
+ // If `body` isn't valid JSON, we just return the text envelope unchanged —
21
+ // callers that emit prose (summaries, error strings) get the old behaviour.
22
+ export function jsonContent(body) {
23
+ const parsed = tryParseJson(body);
24
+ if (parsed === undefined) {
25
+ return { content: [{ type: "text", text: body }] };
26
+ }
27
+ return {
28
+ content: [{ type: "text", text: body }],
29
+ structuredContent: parsed,
30
+ };
31
+ }
32
+ export function tryParseJson(text) {
33
+ const trimmed = text.trim();
34
+ // Only attempt parse if it looks structured. Avoids parsing a stray "true"
35
+ // / number / etc. into structuredContent and confusing the host.
36
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
37
+ return undefined;
38
+ try {
39
+ const v = JSON.parse(trimmed);
40
+ if (v && typeof v === "object")
41
+ return v;
42
+ return undefined;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ export function safeJsonParse(text) {
49
+ try {
50
+ return JSON.parse(text);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ export function sleep(ms) {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+ // Translates a fetch() failure into something an end-user can actually act on.
60
+ // The default "Network error: fetch failed" message tells a non-developer
61
+ // nothing. Look at the underlying cause keyword and map to a concrete fix.
62
+ export function diagnoseNetworkError(err, proxyUrl) {
63
+ const raw = err instanceof Error ? err.message : String(err);
64
+ const lower = raw.toLowerCase();
65
+ // Node's undici surfaces DNS failures as "getaddrinfo ENOTFOUND <host>".
66
+ if (lower.includes("enotfound") || lower.includes("getaddrinfo")) {
67
+ return `Network error: cannot resolve ${proxyUrl}. Check your DNS / corporate proxy / VPN, or confirm the PROXY_URL setting is correct. Raw: ${raw}`;
68
+ }
69
+ // Connection refused / unreachable / TLS handshake failure.
70
+ if (lower.includes("econnrefused") || lower.includes("econnreset")) {
71
+ 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}`;
72
+ }
73
+ if (lower.includes("etimedout") || lower.includes("timeout") || lower.includes("aborterror")) {
74
+ 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}`;
75
+ }
76
+ if (lower.includes("self signed") || lower.includes("cert") || lower.includes("ssl") || lower.includes("tls")) {
77
+ return `Network error: TLS / certificate problem talking to ${proxyUrl}. A corporate MITM proxy may be intercepting traffic. Raw: ${raw}`;
78
+ }
79
+ 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.`;
80
+ }
@@ -0,0 +1,310 @@
1
+ // apps/mcp-server/src/tools/handlers.ts
2
+ //
3
+ // The seven tool-call handlers + the approval long-poll. Extracted from
4
+ // index.ts during the modularization (Critique-Roadmap prompt 06.4). Each
5
+ // takes the ServerContext explicitly (config + server) instead of reaching for
6
+ // module globals. Behavior is unchanged — pinned by test/introspection.test.ts.
7
+ import { errorContent, jsonContent, tryParseJson, safeJsonParse, sleep, diagnoseNetworkError, fetchWithTimeout, } from "../net.js";
8
+ import { requireCredentials, fetchWithAuth } from "../auth.js";
9
+ export async function handleListPlatforms(ctx) {
10
+ const gate = await requireCredentials(ctx);
11
+ if (gate)
12
+ return gate;
13
+ let response;
14
+ try {
15
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
16
+ }
17
+ catch (err) {
18
+ return errorContent(diagnoseNetworkError(err, ctx.PROXY_URL));
19
+ }
20
+ const body = await response.text();
21
+ if (!response.ok)
22
+ return errorContent(`Proxy error ${response.status}: ${body}`);
23
+ // Proxy wraps the payload as { data: { platforms, version }, _meta: {...} }
24
+ // but outputSchema declares the flat { platforms, version } shape, so the
25
+ // wrapped envelope fails strict structuredContent validation and the host
26
+ // surfaces "tool execution failed (no upstream body)". Unwrap .data.
27
+ const parsed = tryParseJson(body);
28
+ const inner = parsed && typeof parsed === "object" && parsed !== null && "data" in parsed
29
+ ? parsed.data
30
+ : parsed;
31
+ if (inner && typeof inner === "object") {
32
+ return {
33
+ content: [{ type: "text", text: body }],
34
+ structuredContent: inner,
35
+ };
36
+ }
37
+ return jsonContent(body);
38
+ }
39
+ // Long-poll budget — under Claude Desktop's hardcoded 60s tool timeout
40
+ // (with 10s of safety). After this we return a graceful "queued" message and
41
+ // the action lands in the user's pending-actions list (Layer 4).
42
+ const APPROVAL_POLL_BUDGET_MS = 50_000;
43
+ const APPROVAL_POLL_INTERVAL_MS = 2_000;
44
+ const APPROVAL_PROGRESS_INTERVAL_MS = 5_000;
45
+ export async function handleUsePlatform(ctx, params, progressToken) {
46
+ const gate = await requireCredentials(ctx);
47
+ if (gate)
48
+ return gate;
49
+ // Observe Mode: when a BYO credential is configured locally, route to the
50
+ // audit-only relay and attach the credential as a header (NEVER in the body —
51
+ // it must not enter model-visible tool args). Governed behaviour is unchanged
52
+ // when no observe credential is set.
53
+ //
54
+ // Platform-match guard: if OBSERVE_PLATFORM is set, only route to the observe
55
+ // relay when the requested platform matches — preventing BYO credential leakage
56
+ // to unrelated platforms. If OBSERVE_PLATFORM is empty, route observe for any
57
+ // platform (backwards-compat when only OBSERVE_CREDENTIAL is set).
58
+ const observePlatformMatch = ctx.OBSERVE_CREDENTIAL &&
59
+ (ctx.OBSERVE_PLATFORM === "" || ctx.OBSERVE_PLATFORM === params.platform);
60
+ if (observePlatformMatch) {
61
+ const observeBody = {
62
+ platform: params.platform,
63
+ endpoint: params.endpoint,
64
+ method: params.method,
65
+ action: params.scope,
66
+ ...(params.data !== undefined && { body: params.data }),
67
+ };
68
+ let response;
69
+ try {
70
+ // fetchWithAuth signs and attaches the AV agent JWT (Authorization: Bearer …).
71
+ // Content-Type: application/json is added by fetchWithAuth before spreading
72
+ // init.headers, so X-AV-Observe-Credential survives.
73
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/observe/actions`, {
74
+ method: "POST",
75
+ headers: { "X-AV-Observe-Credential": ctx.OBSERVE_CREDENTIAL },
76
+ body: JSON.stringify(observeBody),
77
+ });
78
+ }
79
+ catch (err) {
80
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
81
+ }
82
+ const text = await response.text();
83
+ if (!response.ok)
84
+ return errorContent(`Proxy error ${response.status}: ${text}`);
85
+ return jsonContent(text);
86
+ }
87
+ const requestBody = {
88
+ platform: params.platform,
89
+ endpoint: params.endpoint,
90
+ method: params.method,
91
+ scope: params.scope,
92
+ ...(params.data !== undefined && { data: params.data }),
93
+ ...(params.connection_id ? { connection_id: params.connection_id } : {}),
94
+ };
95
+ let response;
96
+ try {
97
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/actions`, {
98
+ method: "POST",
99
+ body: JSON.stringify(requestBody),
100
+ });
101
+ }
102
+ catch (err) {
103
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
104
+ }
105
+ const body = await response.text();
106
+ // 202 + approval_id → enter long-poll. The proxy returns this when the
107
+ // owner needs to approve the call. We poll /v1/approvals/:id every ~2s
108
+ // for up to 50s; if the owner approves in time the proxy re-runs the
109
+ // upstream call and we return its result to the agent transparently.
110
+ if (response.status === 202) {
111
+ const parsed = safeJsonParse(body);
112
+ const approvalId = parsed?.approval_id;
113
+ if (approvalId) {
114
+ return await waitForApproval(ctx, approvalId, params, progressToken);
115
+ }
116
+ }
117
+ if (!response.ok)
118
+ return errorContent(`Proxy error ${response.status}: ${body}`);
119
+ return jsonContent(body);
120
+ }
121
+ async function waitForApproval(ctx, approvalId, originalCall, progressToken) {
122
+ const startedAt = Date.now();
123
+ let lastProgressAt = 0;
124
+ // Initial progress so the user sees activity immediately.
125
+ await sendProgress(ctx, progressToken, 0, APPROVAL_POLL_BUDGET_MS, `Owner approval required for ${originalCall.platform}:${originalCall.scope} — waiting…`);
126
+ while (Date.now() - startedAt < APPROVAL_POLL_BUDGET_MS) {
127
+ await sleep(APPROVAL_POLL_INTERVAL_MS);
128
+ const elapsed = Date.now() - startedAt;
129
+ let res;
130
+ try {
131
+ res = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/approvals/${approvalId}`, { method: "GET" });
132
+ }
133
+ catch {
134
+ // Transient network error — try again next tick.
135
+ continue;
136
+ }
137
+ if (!res.ok) {
138
+ // 404/410 etc. — treat as terminal. Surface back to the agent.
139
+ const text = await res.text();
140
+ return errorContent(`Approval status error ${res.status}: ${text}`);
141
+ }
142
+ const status = (await res.json());
143
+ if (status.status === "approved" && status.executed_at && status.result) {
144
+ // Re-execution complete. Return the upstream response transparently —
145
+ // the agent sees this as if the original use_platform call just succeeded.
146
+ const r = status.result;
147
+ // Mirror the non-2xx-from-upstream behaviour of the regular path.
148
+ if (r.status < 200 || r.status >= 300) {
149
+ return errorContent(`Upstream returned ${r.status}: ${JSON.stringify(r.data)}`);
150
+ }
151
+ // Two-channel return: the text envelope carries the human-readable
152
+ // "[Owner approved after Ns]" prefix (visible to non-structured-aware
153
+ // clients), and structuredContent carries the parsed upstream JSON
154
+ // directly so MCP-aware hosts (Claude Desktop ≥ MCP spec 2025-06-18)
155
+ // never have to strip-the-prefix-and-reparse. Without the second
156
+ // channel the prefix forces Claude into the same wrapper-parse
157
+ // round-trip the bare /v1/actions path used to.
158
+ const data = r.data ?? null;
159
+ const summary = `[Owner approved after ${Math.round(elapsed / 1000)}s]\n` +
160
+ JSON.stringify(data);
161
+ const result = {
162
+ content: [{ type: "text", text: summary }],
163
+ };
164
+ if (data && typeof data === "object" && !Array.isArray(data)) {
165
+ result.structuredContent = data;
166
+ }
167
+ return result;
168
+ }
169
+ if (status.status === "denied") {
170
+ return errorContent(`Owner denied this action${status.execution_error ? `: ${status.execution_error}` : "."}`);
171
+ }
172
+ if (status.status === "expired") {
173
+ return errorContent("Approval request expired before the owner responded.");
174
+ }
175
+ if (status.execution_error) {
176
+ // Approved but re-execution failed (network / upstream / agent revoked).
177
+ return errorContent(`Approved but re-execution failed: ${status.execution_error}`);
178
+ }
179
+ // Still pending — emit a progress update at most once per ~5s so the
180
+ // user can see the wait advancing in the Claude Desktop tool UI.
181
+ if (Date.now() - lastProgressAt >= APPROVAL_PROGRESS_INTERVAL_MS) {
182
+ await sendProgress(ctx, progressToken, elapsed, APPROVAL_POLL_BUDGET_MS, `Waiting for owner approval — ${Math.round(elapsed / 1000)}s elapsed`);
183
+ lastProgressAt = Date.now();
184
+ }
185
+ }
186
+ // Timed out — fall through to async-recap path. The action is still
187
+ // queued and will run when the owner approves; the user is notified via
188
+ // push/email at that point. Layer 4's list_my_pending_actions surfaces it.
189
+ return jsonContent(JSON.stringify({
190
+ status: "pending_approval",
191
+ approval_id: approvalId,
192
+ message: `Owner hasn't approved within ${APPROVAL_POLL_BUDGET_MS / 1000}s. ` +
193
+ `The action is queued — your owner will be notified, and you'll see it ` +
194
+ `complete next time we chat (or you can ask me to check pending actions).`,
195
+ }));
196
+ }
197
+ async function sendProgress(ctx, token, progress, total, message) {
198
+ if (token === undefined)
199
+ return;
200
+ try {
201
+ await ctx.server.notification({
202
+ method: "notifications/progress",
203
+ params: { progressToken: token, progress, total, message },
204
+ });
205
+ }
206
+ catch {
207
+ // Best-effort — never let a failed notification break the call.
208
+ }
209
+ }
210
+ export async function handleAgentRegister(ctx, args) {
211
+ let response;
212
+ try {
213
+ response = await fetchWithTimeout(`${ctx.PROXY_URL}/v1/register`, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify(args),
217
+ });
218
+ }
219
+ catch (err) {
220
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
221
+ }
222
+ const body = await response.text();
223
+ if (!response.ok)
224
+ return errorContent(`Proxy error ${response.status}: ${body}`);
225
+ return jsonContent(body);
226
+ }
227
+ export async function handleAgentStatus(ctx, token) {
228
+ let response;
229
+ try {
230
+ response = await fetchWithTimeout(`${ctx.PROXY_URL}/v1/register/status/${encodeURIComponent(token)}`, {
231
+ method: "GET",
232
+ headers: { "Content-Type": "application/json" },
233
+ });
234
+ }
235
+ catch (err) {
236
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
237
+ }
238
+ const body = await response.text();
239
+ if (!response.ok)
240
+ return errorContent(`Proxy error ${response.status}: ${body}`);
241
+ return jsonContent(body);
242
+ }
243
+ export async function handleAuthzenEvaluate(ctx, platformId, scope) {
244
+ const gate = await requireCredentials(ctx);
245
+ if (gate)
246
+ return gate;
247
+ const authzenBody = {
248
+ subject: { type: "agent", id: ctx.AGENT_ID },
249
+ action: { name: "tool_call" },
250
+ resource: { type: "platform_scope", id: `${platformId}:${scope}` },
251
+ };
252
+ let response;
253
+ try {
254
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/authzen/access`, {
255
+ method: "POST",
256
+ body: JSON.stringify(authzenBody),
257
+ });
258
+ }
259
+ catch (err) {
260
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
261
+ }
262
+ const body = await response.text();
263
+ if (!response.ok)
264
+ return errorContent(`Proxy error ${response.status}: ${body}`);
265
+ return jsonContent(body);
266
+ }
267
+ export async function handleListMyPendingActions(ctx) {
268
+ const gate = await requireCredentials(ctx);
269
+ if (gate)
270
+ return gate;
271
+ let response;
272
+ try {
273
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/agents/me/pending-actions`, { method: "GET" });
274
+ }
275
+ catch (err) {
276
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
277
+ }
278
+ const text = await response.text();
279
+ if (!response.ok)
280
+ return errorContent(`Proxy error ${response.status}: ${text}`);
281
+ return jsonContent(text);
282
+ }
283
+ export async function handleReportSelfDiagnostic(ctx, args) {
284
+ const gate = await requireCredentials(ctx);
285
+ if (gate)
286
+ return gate;
287
+ // Whitelist body fields — never forward owner_id/agent_id (proxy derives those from JWT).
288
+ const body = {
289
+ severity: args.severity,
290
+ message: args.message,
291
+ };
292
+ for (const k of ["code", "platform", "endpoint", "correlation_id", "context"]) {
293
+ if (args[k] !== undefined)
294
+ body[k] = args[k];
295
+ }
296
+ let response;
297
+ try {
298
+ response = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/agents/self/diagnostics`, {
299
+ method: "POST",
300
+ body: JSON.stringify(body),
301
+ });
302
+ }
303
+ catch (err) {
304
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
305
+ }
306
+ const text = await response.text();
307
+ if (!response.ok)
308
+ return errorContent(`Proxy error ${response.status}: ${text}`);
309
+ return jsonContent(text);
310
+ }
@@ -0,0 +1,272 @@
1
+ // apps/mcp-server/src/tools/schemas.ts
2
+ //
3
+ // MCP tool definitions (name + description + input/output JSON schema). Pure
4
+ // data — extracted verbatim from index.ts during the modularization
5
+ // (Critique-Roadmap prompt 06.4). The tool SURFACE is contract-pinned by
6
+ // test/introspection.test.ts, so any accidental change here fails CI.
7
+ export const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
8
+ export const LIST_PLATFORMS_TOOL = {
9
+ name: "list_platforms",
10
+ description: "list_platforms: List the platforms and permission scopes this agent has access to.\nInput: None.\nReturns: { platforms: [{ platformId, platformName, scopes, requireApproval, connections? }], version: \"<hex>\" }. A platform includes `connections` only when it has 2+ usable accounts; pass the chosen connection_id to use_platform (omit for the default).\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.",
11
+ inputSchema: { type: "object", properties: {} },
12
+ outputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ platforms: {
16
+ type: "array",
17
+ items: {
18
+ type: "object",
19
+ properties: {
20
+ platformId: { type: "string" },
21
+ platformName: { type: "string" },
22
+ scopes: { type: "array", items: { type: "string" } },
23
+ requireApproval: { type: "boolean" },
24
+ connections: {
25
+ type: "array",
26
+ description: "Present only when this platform has 2+ usable connections (e.g. two GitHub orgs). Pass the chosen connection_id to use_platform; omit to use the default connection.",
27
+ items: {
28
+ type: "object",
29
+ properties: {
30
+ connection_id: { type: "string" },
31
+ label: { type: "string" },
32
+ is_default: { type: "boolean" },
33
+ scopes: { type: "array", items: { type: "string" } },
34
+ },
35
+ required: ["connection_id", "label"],
36
+ },
37
+ },
38
+ },
39
+ required: ["platformId", "platformName", "scopes"],
40
+ },
41
+ },
42
+ version: { type: "string" },
43
+ },
44
+ required: ["platforms"],
45
+ },
46
+ };
47
+ export const USE_PLATFORM_TOOL = {
48
+ name: "use_platform",
49
+ 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.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ platform: {
54
+ type: "string",
55
+ description: "Platform ID (e.g. airtable, github, slack, metabase)",
56
+ },
57
+ endpoint: {
58
+ type: "string",
59
+ description: "API path on the target platform (e.g. /v0/meta/bases or /api/dataset)",
60
+ },
61
+ method: {
62
+ type: "string",
63
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
64
+ description: "HTTP method to use",
65
+ },
66
+ scope: {
67
+ type: "string",
68
+ description: "Permission scope required for this action (e.g. records:read)",
69
+ },
70
+ body: {
71
+ type: "object",
72
+ description: "JSON request body for POST/PUT/PATCH/DELETE. Optional. Forwarded verbatim to the upstream API.",
73
+ },
74
+ data: {
75
+ type: "object",
76
+ description: "Deprecated alias for `body` — prefer `body`. Kept for backwards compatibility.",
77
+ },
78
+ connection_id: {
79
+ type: "string",
80
+ description: "Optional. Target a specific connection when a platform has more than one connected (e.g. two GitHub orgs). Value is the connection id shown in the dashboard. Omit to use the default connection.",
81
+ },
82
+ },
83
+ required: ["platform", "endpoint", "method", "scope"],
84
+ },
85
+ };
86
+ export const AGENT_REGISTER_TOOL = {
87
+ name: "agent_register",
88
+ 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.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ owner_id: {
93
+ type: "string",
94
+ description: "The owner ID to register this agent under",
95
+ },
96
+ agent_name: {
97
+ type: "string",
98
+ description: "Human-readable name for this agent",
99
+ },
100
+ requested_scopes: {
101
+ type: "array",
102
+ description: "Array of platform scope requests",
103
+ items: {
104
+ type: "object",
105
+ properties: {
106
+ platformId: { type: "string" },
107
+ scopes: { type: "array", items: { type: "string" } },
108
+ },
109
+ required: ["platformId", "scopes"],
110
+ },
111
+ },
112
+ },
113
+ required: ["owner_id", "agent_name", "requested_scopes"],
114
+ },
115
+ outputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ registration_token: { type: "string" },
119
+ poll_url: { type: "string" },
120
+ client_id: { type: "string" },
121
+ scope: { type: "string" },
122
+ expires_in: { type: "number" },
123
+ },
124
+ required: ["registration_token"],
125
+ },
126
+ };
127
+ export const AGENT_STATUS_TOOL = {
128
+ name: "agent_status",
129
+ 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.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ token: {
134
+ type: "string",
135
+ description: "Registration token returned by agent_register",
136
+ },
137
+ },
138
+ required: ["token"],
139
+ },
140
+ outputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ status: { type: "string", enum: ["pending_approval", "approved", "rejected"] },
144
+ agent_id: { type: "string" },
145
+ mcp_config: { type: "object" },
146
+ },
147
+ required: ["status"],
148
+ },
149
+ };
150
+ export const AUTHZEN_EVALUATE_TOOL = {
151
+ name: "authzen_evaluate",
152
+ 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).",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ platform_id: {
157
+ type: "string",
158
+ description: "The platform identifier (e.g. airtable, github)",
159
+ },
160
+ scope: {
161
+ type: "string",
162
+ description: "The permission scope to evaluate (e.g. records:read)",
163
+ },
164
+ },
165
+ required: ["platform_id", "scope"],
166
+ },
167
+ outputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ decision: { type: "boolean" },
171
+ context: { type: "object", properties: { reason: { type: "string" } } },
172
+ },
173
+ required: ["decision"],
174
+ },
175
+ };
176
+ export const REPORT_SELF_DIAGNOSTIC_TOOL = {
177
+ name: "report_self_diagnostic",
178
+ 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).",
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ severity: {
183
+ type: "string",
184
+ enum: ["debug", "info", "warn", "error", "critical"],
185
+ description: "Severity level. error/critical trigger an owner notification.",
186
+ },
187
+ message: {
188
+ type: "string",
189
+ description: "One-sentence agent narrative describing what happened.",
190
+ },
191
+ code: {
192
+ type: "string",
193
+ description: "Optional short machine code (e.g. 'permission_denied').",
194
+ },
195
+ platform: {
196
+ type: "string",
197
+ description: "Optional platform id this report relates to.",
198
+ },
199
+ endpoint: {
200
+ type: "string",
201
+ description: "Optional endpoint that failed.",
202
+ },
203
+ correlation_id: {
204
+ type: "string",
205
+ description: "Optional UUID — copy from a use_platform error's report_hint to stitch this report to the audit row.",
206
+ },
207
+ context: {
208
+ type: "object",
209
+ description: "Optional structured context (request params, error details). Avoid secrets.",
210
+ },
211
+ },
212
+ required: ["severity", "message"],
213
+ },
214
+ outputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ id: { type: "string" },
218
+ received_at: { type: "string" },
219
+ },
220
+ required: ["id"],
221
+ },
222
+ };
223
+ export const LIST_MY_PENDING_ACTIONS_TOOL = {
224
+ name: "list_my_pending_actions",
225
+ 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).",
226
+ inputSchema: { type: "object", properties: {} },
227
+ outputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ pending: {
231
+ type: "array",
232
+ items: {
233
+ type: "object",
234
+ properties: {
235
+ approval_id: { type: "string" },
236
+ platform_id: { type: "string" },
237
+ scope: { type: "string" },
238
+ created_at: { type: "string" },
239
+ expires_at: { type: "string" },
240
+ },
241
+ required: ["approval_id", "platform_id", "scope"],
242
+ },
243
+ },
244
+ recently_completed: {
245
+ type: "array",
246
+ items: {
247
+ type: "object",
248
+ properties: {
249
+ approval_id: { type: "string" },
250
+ platform_id: { type: "string" },
251
+ scope: { type: "string" },
252
+ status: { type: "string" },
253
+ executed_at: { type: "string" },
254
+ result_summary: { type: "string" },
255
+ execution_error: { type: "string" },
256
+ },
257
+ required: ["approval_id", "status"],
258
+ },
259
+ },
260
+ },
261
+ required: ["pending", "recently_completed"],
262
+ },
263
+ };
264
+ export const ALL_TOOLS = [
265
+ LIST_PLATFORMS_TOOL,
266
+ USE_PLATFORM_TOOL,
267
+ AGENT_REGISTER_TOOL,
268
+ AGENT_STATUS_TOOL,
269
+ AUTHZEN_EVALUATE_TOOL,
270
+ REPORT_SELF_DIAGNOSTIC_TOOL,
271
+ LIST_MY_PENDING_ACTIONS_TOOL,
272
+ ];