@agentvalet/mcp-server 1.6.1 → 1.10.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/README.md CHANGED
@@ -1,70 +1,85 @@
1
- # @agentvalet/mcp-server
2
-
3
- MCP server that lets AI agents (Claude Code, Cursor, Codex CLI, etc.) call approved external platforms through the AgentValet proxy.
4
-
5
- ## Quick start
6
-
7
- ```bash
8
- npx @agentvalet/register # registers this machine as an agent, writes config
9
- ```
10
-
11
- The CLI writes env vars and optionally updates `.mcp.json` or `.cursor/mcp.json` automatically.
12
-
13
- ## Manual configuration
14
-
15
- Add to your `.mcp.json` or equivalent:
16
-
17
- ```json
18
- {
19
- "mcpServers": {
20
- "agentvalet": {
21
- "command": "npx",
22
- "args": ["-y", "@agentvalet/mcp-server"],
23
- "env": {
24
- "AGENT_ID": "agt_...",
25
- "OWNER_ID": "...",
26
- "PROXY_URL": "https://api.agentvalet.ai",
27
- "AGENT_PRIVATE_KEY_PATH": "/path/to/agent.key"
28
- }
29
- }
30
- }
31
- }
32
- ```
33
-
34
- ## Environment variables
35
-
36
- | Variable | Required | Description |
37
- |----------|----------|-------------|
38
- | `AGENT_ID` | Yes | Agent ID from registration |
39
- | `OWNER_ID` | Yes | Owner ID from registration |
40
- | `PROXY_URL` | Yes | AgentValet proxy URL |
41
- | `AGENT_PRIVATE_KEY_PATH` | One of three | Path to PEM private key file |
42
- | `AGENT_PRIVATE_KEY` | One of three | Raw PEM or `\n`-escaped single-line PEM |
43
- | `AGENT_PRIVATE_KEY_B64` | One of three | Base64-encoded PEM |
44
- | `MCP_TRANSPORT` | No | Set to `http` to use HTTP transport instead of STDIO |
45
- | `MCP_PORT` | No | Port for HTTP transport (default: `3100`) |
46
-
47
- ## Tools
48
-
49
- | Tool | Auth | Description |
50
- |------|------|-------------|
51
- | `list_platforms` | JWT | List platforms and scopes this agent has access to |
52
- | `use_platform` | JWT | Call an external platform API through the proxy (blocks for owner approval when required) |
53
- | `authzen_evaluate` | JWT | Pre-check whether this agent has access to a platform scope, without making the call |
54
- | `list_my_pending_actions` | JWT | List this agent's pending approvals and actions completed in the last 24h |
55
- | `request_platform_access` | JWT | Ask an org admin to grant access to a platform the agent is blocked from |
56
- | `report_self_diagnostic` | JWT | Lodge a self-report (error/warning/info) with the owner |
57
- | `agent_register` | None | Self-register a new agent with an owner |
58
- | `agent_status` | None | Poll registration approval status |
59
-
60
- ## Transports
61
-
62
- **STDIO (default)** — compatible with all MCP hosts (Claude Code, Cursor, Codex CLI).
63
-
64
- **HTTP** — set `MCP_TRANSPORT=http` and optionally `MCP_PORT=3100`. Uses `StreamableHTTPServerTransport`.
65
-
66
- ## Architecture
67
-
68
- Each tool call signs a short-lived RS256 JWT (`exp: 60s`) using the agent's private key. The JWT is verified by the AgentValet proxy, which checks permissions and forwards the request to the target platform using stored credentials.
69
-
70
- The agent never sees platform credentials. The proxy decrypts them in-memory at call time and discards them after the upstream request completes.
1
+ # @agentvalet/mcp-server
2
+
3
+ MCP server that lets AI agents (Claude Code, Cursor, Codex CLI, etc.) call approved external platforms through the AgentValet proxy.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx @agentvalet/register # registers this machine as an agent, writes config
9
+ ```
10
+
11
+ The CLI writes env vars and optionally updates `.mcp.json` or `.cursor/mcp.json` automatically.
12
+
13
+ ## Manual configuration
14
+
15
+ Add to your `.mcp.json` or equivalent:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "agentvalet": {
21
+ "command": "npx",
22
+ "args": ["-y", "@agentvalet/mcp-server"],
23
+ "env": {
24
+ "AGENT_ID": "agt_...",
25
+ "OWNER_ID": "...",
26
+ "PROXY_URL": "https://api.agentvalet.ai",
27
+ "AGENT_PRIVATE_KEY_PATH": "/path/to/agent.key"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Environment variables
35
+
36
+ | Variable | Required | Description |
37
+ |----------|----------|-------------|
38
+ | `AGENT_ID` | Yes | Agent ID from registration |
39
+ | `OWNER_ID` | Yes | Owner ID from registration |
40
+ | `PROXY_URL` | Yes | AgentValet proxy URL |
41
+ | `AGENT_PRIVATE_KEY_PATH` | One of three | Path to PEM private key file |
42
+ | `AGENT_PRIVATE_KEY` | One of three | Raw PEM or `\n`-escaped single-line PEM |
43
+ | `AGENT_PRIVATE_KEY_B64` | One of three | Base64-encoded PEM |
44
+ | `MCP_TRANSPORT` | No | Set to `http` to use HTTP transport instead of STDIO |
45
+ | `MCP_PORT` | No | Port for HTTP transport (default: `3100`) |
46
+
47
+ ## Tools
48
+
49
+ | Tool | Auth | Description |
50
+ |------|------|-------------|
51
+ | `list_platforms` | JWT | List platforms and scopes this agent has access to |
52
+ | `read_platform` | JWT | Read from an external platform API through the proxy (always GET) |
53
+ | `write_platform` | JWT | Create or update through the proxy (POST/PUT/PATCH; blocks for owner approval when required) |
54
+ | `delete_platform` | JWT | Delete through the proxy (always DELETE; blocks for owner approval when required) |
55
+ | `use_platform` | JWT | Deprecated catch-all. Use the three tools above; removed in the next major. |
56
+ | `authzen_evaluate` | JWT | Pre-check whether this agent has access to a platform scope, without making the call |
57
+ | `list_my_pending_actions` | JWT | List this agent's pending approvals and actions completed in the last 24h |
58
+ | `request_platform_access` | JWT | Ask an org admin to grant access to a platform the agent is blocked from |
59
+ | `report_self_diagnostic` | JWT | Lodge a self-report (error/warning/info) with the owner |
60
+ | `agent_register` | None | Self-register a new agent with an owner |
61
+ | `agent_status` | None | Poll registration approval status |
62
+ | `issue_child_agent` | JWT | Mint a short-lived child identity for a subagent: its own agent id, an attenuated scope slice (child ⊆ parent), its own audit trail, individually revocable. Returns a bearer token for the subagent's `Authorization` header. |
63
+
64
+ ## Session attribution
65
+
66
+ Every assertion this server signs carries a stable per-process `session_id`
67
+ claim, which the proxy stamps on each audit row. It distinguishes *runs* of
68
+ the server, not subagents: hosts that share one server process (for example
69
+ Claude Code Task-tool subagents) share the id. HTTP callers without this
70
+ server can supply the same discriminator via an `X-AV-Session` header
71
+ (`[A-Za-z0-9._:-]{1,128}`). Both are caller-asserted forensics — the proxy
72
+ never makes an authorization decision on them. For verified per-subagent
73
+ identity, mint a child with `issue_child_agent` instead.
74
+
75
+ ## Transports
76
+
77
+ **STDIO (default)** — compatible with all MCP hosts (Claude Code, Cursor, Codex CLI).
78
+
79
+ **HTTP** — set `MCP_TRANSPORT=http` and optionally `MCP_PORT=3100`. Uses `StreamableHTTPServerTransport`.
80
+
81
+ ## Architecture
82
+
83
+ Each tool call signs a short-lived RS256 JWT (`exp: 60s`) using the agent's private key. The JWT is verified by the AgentValet proxy, which checks permissions and forwards the request to the target platform using stored credentials.
84
+
85
+ The agent never sees platform credentials. The proxy decrypts them in-memory at call time and discards them after the upstream request completes.
package/dist/auth.js CHANGED
@@ -9,7 +9,7 @@ import { fetchWithTimeout } from "./net.js";
9
9
  export async function signJWT(ctx) {
10
10
  if (!ctx.privateKey)
11
11
  throw new Error("Private key not loaded");
12
- return new SignJWT({ agent_id: ctx.AGENT_ID, owner_id: ctx.OWNER_ID })
12
+ return new SignJWT({ agent_id: ctx.AGENT_ID, owner_id: ctx.OWNER_ID, session_id: ctx.SESSION_ID })
13
13
  .setProtectedHeader({ alg: "RS256" })
14
14
  .setIssuedAt()
15
15
  .setExpirationTime("60s")
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ process.on("unhandledRejection", (err) => {
15
15
  process.stderr.write(`[mcp-server] FATAL unhandledRejection: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
16
16
  process.exit(1);
17
17
  });
18
+ import { randomBytes } from "node:crypto";
18
19
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
21
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -22,7 +23,7 @@ import { validateConfig } from "./config.js";
22
23
  import { renderInstructions } from "./instructions.js";
23
24
  import { signJWT } from "./auth.js";
24
25
  import { ALLOWED_METHODS, ALL_TOOLS } from "./tools/schemas.js";
25
- import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, handleRequestPlatformAccess, } from "./tools/handlers.js";
26
+ import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, handleRequestPlatformAccess, handleIssueChildAgent, } from "./tools/handlers.js";
26
27
  import { errorContent } from "./net.js";
27
28
  import { getPermissionsVersion, recordPermissionsVersion, startPermissionsWatcher, } from "./permissions-watcher.js";
28
29
  // ---------------------------------------------------------------------------
@@ -47,12 +48,18 @@ const server = new Server({ name: "agentvalet", version: "1.0.0" },
47
48
  // permissions-watcher.ts). Declaring the capability is what makes the SDK
48
49
  // allow that notification.
49
50
  { capabilities: { tools: { listChanged: true } }, instructions: renderInstructions(undefined) });
51
+ // One id per server process. Discriminates runs in audit_log.session_id;
52
+ // forensics only — the proxy never branches enforcement on it. Note: Claude
53
+ // Code Task-tool subagents share the parent process, so this discriminates
54
+ // processes/runs, not subagents.
55
+ const SESSION_ID = "sess_" + randomBytes(8).toString("hex");
50
56
  // The config + server bundle threaded into auth + handlers (see context.ts) —
51
57
  // keeps those modules free of globals.
52
58
  const ctx = {
53
59
  AGENT_ID,
54
60
  OWNER_ID,
55
61
  PROXY_URL,
62
+ SESSION_ID,
56
63
  AGENT_PRIVATE_KEY_RAW,
57
64
  privateKey,
58
65
  server,
@@ -102,7 +109,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
102
109
  ...tool,
103
110
  description: `${tool.description}\nCurrent permissions version: ${version}. If you are holding a ` +
104
111
  `list_platforms result from a different version, it is STALE — call list_platforms ` +
105
- `again before relying on its scopes or requireApproval flags.`,
112
+ `again before relying on its scopes, requireApproval flags, or approvalScopes ` +
113
+ `(the scopes whose calls need human approval under the owner's policy).`,
106
114
  }
107
115
  : tool),
108
116
  };
@@ -146,6 +154,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
146
154
  ...(typeof args.reason === "string" ? { reason: args.reason } : {}),
147
155
  }, progressToken);
148
156
  }
157
+ // The split tools. handleUsePlatform already takes the method as a parameter,
158
+ // so the method is supplied HERE rather than by the model — which is what
159
+ // makes read_platform genuinely read-only.
160
+ if (name === "read_platform" || name === "write_platform" || name === "delete_platform") {
161
+ if (!args || typeof args.platform !== "string" || typeof args.endpoint !== "string" || typeof args.scope !== "string") {
162
+ return errorContent("Invalid or missing arguments: platform, endpoint, scope are required");
163
+ }
164
+ let method;
165
+ if (name === "read_platform") {
166
+ method = "GET";
167
+ }
168
+ else if (name === "delete_platform") {
169
+ method = "DELETE";
170
+ }
171
+ else {
172
+ const requested = (args.method ?? "POST");
173
+ if (requested !== "POST" && requested !== "PUT" && requested !== "PATCH") {
174
+ return errorContent(`write_platform does not accept ${requested}. Use read_platform for GET, delete_platform for DELETE.`);
175
+ }
176
+ method = requested;
177
+ }
178
+ const progressToken = request.params._meta?.progressToken;
179
+ return await handleUsePlatform(ctx, {
180
+ platform: args.platform,
181
+ endpoint: args.endpoint,
182
+ method,
183
+ scope: args.scope,
184
+ // No `data` alias on the new tools: new surface, no legacy.
185
+ ...(args.body !== undefined ? { data: args.body } : {}),
186
+ ...(typeof args.connection_id === "string" ? { connection_id: args.connection_id } : {}),
187
+ ...(typeof args.reason === "string" ? { reason: args.reason } : {}),
188
+ }, progressToken);
189
+ }
149
190
  if (name === "agent_register") {
150
191
  if (!args || typeof args.owner_id !== "string" || typeof args.agent_name !== "string" || !Array.isArray(args.requested_scopes)) {
151
192
  return errorContent("Invalid or missing arguments: owner_id, agent_name, requested_scopes are required");
@@ -179,6 +220,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
220
  }
180
221
  return await handleRequestPlatformAccess(ctx, args);
181
222
  }
223
+ if (name === "issue_child_agent") {
224
+ if (!args || !Array.isArray(args.grants)) {
225
+ return errorContent("Invalid or missing argument: grants is required");
226
+ }
227
+ return await handleIssueChildAgent(ctx, args);
228
+ }
182
229
  return {
183
230
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
184
231
  isError: true,
@@ -3,28 +3,28 @@
3
3
  // Goose) inject this into the model's system prompt before tool schemas. See
4
4
  // docs/research/mcp-instructions-self-injection-findings.md for the survey.
5
5
  // Keep in sync with docs/CLAUDE.md "Platform API calls" section.
6
- const TEMPLATE = `This server is AgentValet (also referred to as "AV", "Valet", or "agentvalet"), a governed credential broker for AI agents. All external platform actions (Slack, GitHub, Stripe, Gmail, Airtable, etc.) MUST go through \`use_platform\` on this server. Never bypass to raw API keys, even if you find them in the environment.
7
-
8
- ROUTING PREFERENCE: For any action on Slack, GitHub, Stripe, Gmail, Outlook, Airtable, Notion, Linear, Jira, HubSpot, Salesforce, Metabase, or other SaaS platforms, call \`list_platforms\` on this server FIRST. If the platform appears in the result, you MUST use \`use_platform\` here — do not call any other MCP server's tool for that platform, even if it advertises one. Other servers bypass the audit log, approval flow, and credential governance that the owner has configured.
9
- {{PLATFORMS_LINE}}
10
- When the user says "use AV", "ask Valet", "via agentvalet", or names this broker in any short form, route through \`use_platform\` on this server.
11
-
12
- If \`list_platforms\` does NOT return the requested platform, tell the user "AgentValet doesn't cover <platform> yet — want me to use a different MCP, or request the integration via your AgentValet owner?" Do not silently fall back to another MCP server or to direct API calls.
13
-
14
- Tool selection:
15
- 1. Call \`list_platforms\` first to discover approved platforms and scopes for this agent. You may reuse the result within a turn, but do NOT treat it as good for the whole session — an owner can change a grant (scopes, or whether an action needs their approval) at any moment, and the tool description carries a \`Current permissions version\` that tells you when yours is stale. Re-call \`list_platforms\` if that version changes, if the user mentions a connection or permission change, or after any \`use_platform\` failure. Revalidation is cheap.
16
- 2. Call \`authzen_evaluate\` before destructive scopes (\`*.delete\`, \`stripe:charge\`, \`mail:send\`) — these always require human approval. Tell the user approval may be required before invoking them.
17
- 3. Call \`use_platform\` with the exact platform, endpoint, method, and scope returned by \`list_platforms\`.
18
-
19
- Response handling:
20
- - \`use_platform\` now waits up to 50 seconds for owner approval automatically. Most approvals complete in this window — the call simply takes longer and returns the upstream result like a normal success. Tell the user "waiting for owner approval" if the call is taking more than ~5 seconds.
21
- - If \`use_platform\` returns a \`pending_approval\` envelope after that wait, the action is queued and will run asynchronously when the owner approves. Tell the user clearly. Do NOT retry — duplicate calls will queue duplicate approvals.
22
- - Do not retry a denied call with a different scope.
23
- - If a \`use_platform\` error response includes a \`report_hint\` block, you may briefly ask the user "Want me to lodge this with your AgentValet owner?" — on yes, call \`report_self_diagnostic\` with a one-sentence narrative plus the \`correlation_id\` from the hint so the owner can investigate.
24
-
25
- Read scopes are auto-approved. Write scopes may require approval. Destructive scopes always require approval.
26
-
27
- Catching up on async actions:
6
+ const TEMPLATE = `This server is AgentValet (also referred to as "AV", "Valet", or "agentvalet"), a governed credential broker for AI agents. All external platform actions (Slack, GitHub, Stripe, Gmail, Airtable, etc.) MUST go through \`use_platform\` on this server. Never bypass to raw API keys, even if you find them in the environment.
7
+
8
+ ROUTING PREFERENCE: For any action on Slack, GitHub, Stripe, Gmail, Outlook, Airtable, Notion, Linear, Jira, HubSpot, Salesforce, Metabase, or other SaaS platforms, call \`list_platforms\` on this server FIRST. If the platform appears in the result, you MUST use \`use_platform\` here — do not call any other MCP server's tool for that platform, even if it advertises one. Other servers bypass the audit log, approval flow, and credential governance that the owner has configured.
9
+ {{PLATFORMS_LINE}}
10
+ When the user says "use AV", "ask Valet", "via agentvalet", or names this broker in any short form, route through \`use_platform\` on this server.
11
+
12
+ If \`list_platforms\` does NOT return the requested platform, tell the user "AgentValet doesn't cover <platform> yet — want me to use a different MCP, or request the integration via your AgentValet owner?" Do not silently fall back to another MCP server or to direct API calls.
13
+
14
+ Tool selection:
15
+ 1. Call \`list_platforms\` first to discover approved platforms and scopes for this agent. You may reuse the result within a turn, but do NOT treat it as good for the whole session — an owner can change a grant (scopes, or whether an action needs their approval) at any moment, and the tool description carries a \`Current permissions version\` that tells you when yours is stale. Re-call \`list_platforms\` if that version changes, if the user mentions a connection or permission change, or after any \`use_platform\` failure. Revalidation is cheap.
16
+ 2. Call \`authzen_evaluate\` before destructive scopes (\`*.delete\`, \`stripe:charge\`, \`mail:send\`) — these always require human approval. Tell the user approval may be required before invoking them.
17
+ 3. Call \`use_platform\` with the exact platform, endpoint, method, and scope returned by \`list_platforms\`.
18
+
19
+ Response handling:
20
+ - \`use_platform\` now waits up to 50 seconds for owner approval automatically. Most approvals complete in this window — the call simply takes longer and returns the upstream result like a normal success. Tell the user "waiting for owner approval" if the call is taking more than ~5 seconds.
21
+ - If \`use_platform\` returns a \`pending_approval\` envelope after that wait, the action is queued and will run asynchronously when the owner approves. Tell the user clearly. Do NOT retry — duplicate calls will queue duplicate approvals.
22
+ - Do not retry a denied call with a different scope.
23
+ - If a \`use_platform\` error response includes a \`report_hint\` block, you may briefly ask the user "Want me to lodge this with your AgentValet owner?" — on yes, call \`report_self_diagnostic\` with a one-sentence narrative plus the \`correlation_id\` from the hint so the owner can investigate.
24
+
25
+ Read scopes are auto-approved. Write scopes may require approval. Destructive scopes always require approval.
26
+
27
+ Catching up on async actions:
28
28
  - If the user mentions a previous action ("did the Slack post go through?", "what happened to that earlier request?") OR if the previous \`use_platform\` returned a \`pending_approval\` envelope, call \`list_my_pending_actions\` to surface what's pending and what's recently completed. Tell the user the result naturally — don't dump the JSON.`;
29
29
  /**
30
30
  * Render the instructions string. If a platform list is provided, the
@@ -9,7 +9,8 @@
9
9
  // nothing ever told the agent the spec had moved.
10
10
  //
11
11
  // GET /v1/agent/permissions returns a deterministic `version` hash over the
12
- // effective spec (scopes + requireApproval + connections). We poll it with
12
+ // effective spec (scopes + requireApproval + policy-level approvalScopes +
13
+ // connections incl. per-connection requireApproval). We poll it with
13
14
  // If-None-Match, so an unchanged spec costs a 304 with no body. When the hash
14
15
  // does move we emit notifications/tools/list_changed; the host re-reads
15
16
  // tools/list, and because the list_platforms description is stamped with the
@@ -347,6 +347,17 @@ export async function handleRequestPlatformAccess(ctx, args) {
347
347
  message: "Request submitted and awaiting admin approval. Retry your call later.",
348
348
  }));
349
349
  }
350
+ export async function handleIssueChildAgent(ctx, args) {
351
+ const gate = await requireCredentials(ctx);
352
+ if (gate)
353
+ return gate;
354
+ const resp = await fetchWithAuth(ctx, `${ctx.PROXY_URL}/v1/agents/children`, {
355
+ method: "POST",
356
+ body: JSON.stringify(args ?? {}),
357
+ });
358
+ const json = await resp.json().catch(() => ({ error: `HTTP ${resp.status}` }));
359
+ return { content: [{ type: "text", text: JSON.stringify(json) }], isError: !resp.ok };
360
+ }
350
361
  export async function handleReportSelfDiagnostic(ctx, args) {
351
362
  const gate = await requireCredentials(ctx);
352
363
  if (gate)
@@ -7,7 +7,7 @@
7
7
  export const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
8
8
  export const LIST_PLATFORMS_TOOL = {
9
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 over the effective spec — it changes whenever the platform set, the scopes, or the requireApproval flags change. Compare it against the `Current permissions version` stamped on this description: if they differ, your cached result is stale and you must call list_platforms again before trusting it.\nAuth: Bearer JWT.",
10
+ description: "list_platforms: List the platforms and permission scopes this agent has access to.\nInput: None.\nReturns: { platforms: [{ platformId, platformName, scopes, requireApproval, approvalScopes?, 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).\nApproval: a call needs human approval if the platform's requireApproval is true (grant-level: every call prompts), if the chosen connection's requireApproval is true, or if the scope is listed in approvalScopes (policy-level: just those scopes prompt). approvalScopes is omitted when no scope is policy-gated.\nVersion: a deterministic hash over the effective spec — it changes whenever the platform set, the scopes, the approval requirements (grant or policy), or the connections change. Compare it against the `Current permissions version` stamped on this description: if they differ, your cached result is stale and you must call list_platforms again before trusting it.\nAuth: Bearer JWT.",
11
11
  inputSchema: { type: "object", properties: {} },
12
12
  outputSchema: {
13
13
  type: "object",
@@ -21,6 +21,11 @@ export const LIST_PLATFORMS_TOOL = {
21
21
  platformName: { type: "string" },
22
22
  scopes: { type: "array", items: { type: "string" } },
23
23
  requireApproval: { type: "boolean" },
24
+ approvalScopes: {
25
+ type: "array",
26
+ description: "Present only when the owner's policy or an org guardrail gates some scopes on human approval. Subset of `scopes`: calls under these scopes prompt the owner even when requireApproval is false.",
27
+ items: { type: "string" },
28
+ },
24
29
  connections: {
25
30
  type: "array",
26
31
  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.",
@@ -30,6 +35,7 @@ export const LIST_PLATFORMS_TOOL = {
30
35
  connection_id: { type: "string" },
31
36
  label: { type: "string" },
32
37
  is_default: { type: "boolean" },
38
+ requireApproval: { type: "boolean" },
33
39
  scopes: { type: "array", items: { type: "string" } },
34
40
  },
35
41
  required: ["connection_id", "label"],
@@ -87,6 +93,89 @@ export const USE_PLATFORM_TOOL = {
87
93
  required: ["platform", "endpoint", "method", "scope"],
88
94
  },
89
95
  };
96
+ // --- The read/write/delete split -------------------------------------------
97
+ //
98
+ // Descriptions are copied VERBATIM from apps/proxy/src/mcp/tools.ts so the two
99
+ // servers cannot drift. read_platform and delete_platform declare no `method`
100
+ // property at all: the method is supplied at dispatch, which is what makes the
101
+ // readOnlyHint annotation enforced rather than merely promised.
102
+ export const READ_PLATFORM_TOOL = {
103
+ name: "read_platform",
104
+ description: "read_platform: Read data from a platform the owner has connected, through the AgentValet proxy. Calls the platform's own public REST API (for example the GitHub REST API, the Slack Web API, the Stripe API); `endpoint` is a path in that API exactly as its documentation defines it. Always issues GET — use write_platform to create or update, and delete_platform to remove. Input: platform, endpoint, scope, connection_id (optional), reason (optional). Returns: the upstream API response body. Auth: Bearer JWT.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ platform: { type: "string", description: "Platform ID (e.g. github, slack, stripe)" },
109
+ endpoint: { type: "string", description: "Path in the platform's own API (e.g. /user/repos)" },
110
+ scope: { type: "string", description: "Permission scope required for this read (e.g. repo:read)" },
111
+ connection_id: {
112
+ type: "string",
113
+ description: "Optional. Target a specific connection when a platform has more than one connected. Omit to use the default.",
114
+ },
115
+ reason: {
116
+ type: "string",
117
+ description: "Optional but strongly encouraged. One sentence on WHY you are making this call. Shown to the human approver if the call is gated. Max 500 characters; plain text.",
118
+ },
119
+ },
120
+ required: ["platform", "endpoint", "scope"],
121
+ },
122
+ };
123
+ export const WRITE_PLATFORM_TOOL = {
124
+ name: "write_platform",
125
+ description: "write_platform: Create or update data on a platform the owner has connected, through the AgentValet proxy. Calls the platform's own public REST API (for example the GitHub REST API, the Slack Web API, the Stripe API); `endpoint` is a path in that API exactly as its documentation defines it. Accepts POST, PUT or PATCH only — use read_platform for GET and delete_platform for DELETE. May block up to 50s for owner approval, then returns the approved result; otherwise returns a pending_approval envelope. Input: platform, endpoint, scope, method (optional, default POST), body (optional), connection_id (optional), reason (optional). Auth: Bearer JWT.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ platform: { type: "string", description: "Platform ID (e.g. github, slack, stripe)" },
130
+ endpoint: { type: "string", description: "Path in the platform's own API (e.g. /api/chat.postMessage)" },
131
+ method: {
132
+ type: "string",
133
+ enum: ["POST", "PUT", "PATCH"],
134
+ default: "POST",
135
+ description: "POST to create, PUT to replace, PATCH to modify. Defaults to POST.",
136
+ },
137
+ scope: { type: "string", description: "Permission scope required for this write (e.g. chat:write)" },
138
+ body: {
139
+ type: "object",
140
+ description: "JSON request body, forwarded verbatim to the upstream API.",
141
+ },
142
+ connection_id: {
143
+ type: "string",
144
+ description: "Optional. Target a specific connection when a platform has more than one connected. Omit to use the default.",
145
+ },
146
+ reason: {
147
+ type: "string",
148
+ description: "Optional but strongly encouraged. One sentence on WHY you are making this call. Shown to the human approver if the call is gated. Max 500 characters; plain text.",
149
+ },
150
+ },
151
+ required: ["platform", "endpoint", "scope"],
152
+ },
153
+ };
154
+ export const DELETE_PLATFORM_TOOL = {
155
+ name: "delete_platform",
156
+ description: "delete_platform: Delete data on a platform the owner has connected, through the AgentValet proxy. Calls the platform's own public REST API (for example the GitHub REST API, the Slack Web API, the Stripe API); `endpoint` is a path in that API exactly as its documentation defines it. Always issues DELETE. This removes data and is not reversible by AgentValet. May block up to 50s for owner approval. Input: platform, endpoint, scope, body (optional), connection_id (optional), reason (optional). Auth: Bearer JWT.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ platform: { type: "string", description: "Platform ID (e.g. github, slack, stripe)" },
161
+ endpoint: { type: "string", description: "Path in the platform's own API (e.g. /repos/owner/name)" },
162
+ scope: { type: "string", description: "Permission scope required for this delete (e.g. repo:delete)" },
163
+ body: {
164
+ type: "object",
165
+ description: "Optional JSON request body, forwarded verbatim.",
166
+ },
167
+ connection_id: {
168
+ type: "string",
169
+ description: "Optional. Target a specific connection when a platform has more than one connected. Omit to use the default.",
170
+ },
171
+ reason: {
172
+ type: "string",
173
+ description: "Optional but strongly encouraged. One sentence on WHY you are deleting this. Shown to the human approver if the call is gated. Max 500 characters; plain text.",
174
+ },
175
+ },
176
+ required: ["platform", "endpoint", "scope"],
177
+ },
178
+ };
90
179
  export const AGENT_REGISTER_TOOL = {
91
180
  name: "agent_register",
92
181
  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.",
@@ -286,13 +375,45 @@ export const REQUEST_PLATFORM_ACCESS_TOOL = {
286
375
  required: ["status"],
287
376
  },
288
377
  };
378
+ export const ISSUE_CHILD_AGENT_TOOL = {
379
+ name: "issue_child_agent",
380
+ description: "Issue a short-lived child agent identity for a subagent you are about to spawn. The child gets its own agent id, its own attenuated permission slice (child scopes must be a subset of yours), and its own audit trail; it can be revoked alone without touching you or its siblings. Returns a bearer token the subagent must send as its Authorization header. TTL 60-3600s (default 900). Inject ONLY the bearer_token into the subagent's context — never your own credentials.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ name: { type: "string", description: "Optional human-readable name for the child agent (1-200 chars)." },
385
+ ttl_seconds: {
386
+ type: "number",
387
+ description: "Optional lifetime in seconds for the child's bearer token. 60-3600, default 900.",
388
+ },
389
+ grants: {
390
+ type: "array",
391
+ description: "Array of platform grants to attenuate onto the child. Each scope must be a subset of your own granted scopes.",
392
+ items: {
393
+ type: "object",
394
+ properties: {
395
+ platform: { type: "string", description: "Platform ID (e.g. github, slack)" },
396
+ scopes: { type: "array", items: { type: "string" }, description: "Scopes to grant the child for this platform." },
397
+ connection_id: { type: "string", description: "Optional. Target a specific connection when a platform has more than one connected." },
398
+ },
399
+ required: ["platform", "scopes"],
400
+ },
401
+ },
402
+ },
403
+ required: ["grants"],
404
+ },
405
+ };
289
406
  export const ALL_TOOLS = [
290
407
  LIST_PLATFORMS_TOOL,
291
408
  USE_PLATFORM_TOOL,
409
+ READ_PLATFORM_TOOL,
410
+ WRITE_PLATFORM_TOOL,
411
+ DELETE_PLATFORM_TOOL,
292
412
  AGENT_REGISTER_TOOL,
293
413
  AGENT_STATUS_TOOL,
294
414
  AUTHZEN_EVALUATE_TOOL,
295
415
  REPORT_SELF_DIAGNOSTIC_TOOL,
296
416
  LIST_MY_PENDING_ACTIONS_TOOL,
297
417
  REQUEST_PLATFORM_ACCESS_TOOL,
418
+ ISSUE_CHILD_AGENT_TOOL,
298
419
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/mcp-server",
3
- "version": "1.6.1",
3
+ "version": "1.10.0",
4
4
  "description": "AgentValet MCP server — lets AI agents call approved platforms via the AgentValet proxy",
5
5
  "mcpName": "io.github.agentvalet/mcp-server",
6
6
  "type": "module",
@@ -12,6 +12,14 @@
12
12
  "dist",
13
13
  "README.md"
14
14
  ],
15
+ "scripts": {
16
+ "dev": "tsx src/index.ts",
17
+ "build": "tsc",
18
+ "start": "node dist/index.js",
19
+ "test": "vitest run",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "node ../../scripts/no-secrets.mjs && npm run build && node ../../scripts/no-workspace-protocol.mjs"
22
+ },
15
23
  "dependencies": {
16
24
  "@modelcontextprotocol/sdk": "^1.0.0",
17
25
  "jose": "^5.10.0"
@@ -37,12 +45,5 @@
37
45
  "agentvalet"
38
46
  ],
39
47
  "license": "MIT",
40
- "homepage": "https://github.com/AgentValet/AgentValet#readme",
41
- "scripts": {
42
- "dev": "tsx src/index.ts",
43
- "build": "tsc",
44
- "start": "node dist/index.js",
45
- "test": "vitest run",
46
- "typecheck": "tsc --noEmit"
47
- }
48
- }
48
+ "homepage": "https://github.com/AgentValet/AgentValet#readme"
49
+ }