@agentvalet/mcp-server 1.4.0 → 1.6.1
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 +70 -67
- package/dist/index.js +38 -5
- package/dist/instructions.js +1 -1
- package/dist/permissions-watcher.js +132 -0
- package/dist/tools/handlers.js +7 -0
- package/dist/tools/schemas.js +6 -2
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -1,67 +1,70 @@
|
|
|
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 |
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { signJWT } from "./auth.js";
|
|
|
24
24
|
import { ALLOWED_METHODS, ALL_TOOLS } from "./tools/schemas.js";
|
|
25
25
|
import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, handleRequestPlatformAccess, } from "./tools/handlers.js";
|
|
26
26
|
import { errorContent } from "./net.js";
|
|
27
|
+
import { getPermissionsVersion, recordPermissionsVersion, startPermissionsWatcher, } from "./permissions-watcher.js";
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Startup env validation
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -40,7 +41,12 @@ process.stderr.write(`[mcp-server] config ok | agent=${AGENT_ID} | owner=${OWNER
|
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
41
42
|
// MCP server setup
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
|
-
const server = new Server({ name: "agentvalet", version: "1.0.0" },
|
|
44
|
+
const server = new Server({ name: "agentvalet", version: "1.0.0" },
|
|
45
|
+
// listChanged: the permissions watcher pushes notifications/tools/list_changed
|
|
46
|
+
// when the agent's effective platform spec moves under it (see
|
|
47
|
+
// permissions-watcher.ts). Declaring the capability is what makes the SDK
|
|
48
|
+
// allow that notification.
|
|
49
|
+
{ capabilities: { tools: { listChanged: true } }, instructions: renderInstructions(undefined) });
|
|
44
50
|
// The config + server bundle threaded into auth + handlers (see context.ts) —
|
|
45
51
|
// keeps those modules free of globals.
|
|
46
52
|
const ctx = {
|
|
@@ -65,19 +71,42 @@ void (async () => {
|
|
|
65
71
|
const token = await signJWT(ctx);
|
|
66
72
|
const ac = new AbortController();
|
|
67
73
|
const timer = setTimeout(() => ac.abort(), 4_000);
|
|
68
|
-
await fetch(`${PROXY_URL}/v1/agent/permissions`, {
|
|
74
|
+
const res = await fetch(`${PROXY_URL}/v1/agent/permissions`, {
|
|
69
75
|
method: "GET",
|
|
70
76
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
71
77
|
signal: ac.signal,
|
|
72
78
|
}).finally(() => clearTimeout(timer));
|
|
79
|
+
// Seed the watcher's baseline so the first poll compares against the spec
|
|
80
|
+
// as it was at boot, not against "unknown".
|
|
81
|
+
if (res.ok) {
|
|
82
|
+
const parsed = (await res.json());
|
|
83
|
+
recordPermissionsVersion(parsed?.data?.version ?? parsed?.version);
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
catch {
|
|
75
87
|
// best-effort warmup only
|
|
76
88
|
}
|
|
77
89
|
})().catch(() => { });
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// Stamp list_platforms with the current permissions version. This is the half
|
|
91
|
+
// of the staleness fix that actually reaches the model: after a
|
|
92
|
+
// tools/list_changed push the host re-reads tools/list, and a description that
|
|
93
|
+
// genuinely differs from the one already in context is what tells the model its
|
|
94
|
+
// cached platform spec is no longer the live one.
|
|
95
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
96
|
+
const version = getPermissionsVersion();
|
|
97
|
+
if (!version)
|
|
98
|
+
return { tools: ALL_TOOLS };
|
|
99
|
+
return {
|
|
100
|
+
tools: ALL_TOOLS.map((tool) => tool.name === "list_platforms"
|
|
101
|
+
? {
|
|
102
|
+
...tool,
|
|
103
|
+
description: `${tool.description}\nCurrent permissions version: ${version}. If you are holding a ` +
|
|
104
|
+
`list_platforms result from a different version, it is STALE — call list_platforms ` +
|
|
105
|
+
`again before relying on its scopes or requireApproval flags.`,
|
|
106
|
+
}
|
|
107
|
+
: tool),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
81
110
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
82
111
|
const { name, arguments: args } = request.params;
|
|
83
112
|
if (name === "list_platforms") {
|
|
@@ -114,6 +143,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
114
143
|
scope: args.scope,
|
|
115
144
|
data: bodyArg,
|
|
116
145
|
...(typeof args.connection_id === "string" ? { connection_id: args.connection_id } : {}),
|
|
146
|
+
...(typeof args.reason === "string" ? { reason: args.reason } : {}),
|
|
117
147
|
}, progressToken);
|
|
118
148
|
}
|
|
119
149
|
if (name === "agent_register") {
|
|
@@ -166,3 +196,6 @@ else {
|
|
|
166
196
|
const transport = new StdioServerTransport();
|
|
167
197
|
await server.connect(transport);
|
|
168
198
|
}
|
|
199
|
+
// Started only after the transport is connected — notifications have nowhere to
|
|
200
|
+
// go before that. No-op without credentials or when AV_PERMISSIONS_POLL_MS=0.
|
|
201
|
+
startPermissionsWatcher(ctx);
|
package/dist/instructions.js
CHANGED
|
@@ -12,7 +12,7 @@ When the user says "use AV", "ask Valet", "via agentvalet", or names this broker
|
|
|
12
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
13
|
|
|
14
14
|
Tool selection:
|
|
15
|
-
1. Call \`list_platforms\` first to discover approved platforms and scopes for this agent.
|
|
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
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
17
|
3. Call \`use_platform\` with the exact platform, endpoint, method, and scope returned by \`list_platforms\`.
|
|
18
18
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// apps/mcp-server/src/permissions-watcher.ts
|
|
2
|
+
//
|
|
3
|
+
// Keeps the host's view of the agent's platform spec from going stale.
|
|
4
|
+
//
|
|
5
|
+
// The problem this exists for: an owner flips a grant in the dashboard (say,
|
|
6
|
+
// require_approval on Slack) mid-session. The agent had already called
|
|
7
|
+
// list_platforms and cached the answer in its context, so it kept reasoning
|
|
8
|
+
// from the pre-change spec — the proxy enforced the new rule at call time, but
|
|
9
|
+
// nothing ever told the agent the spec had moved.
|
|
10
|
+
//
|
|
11
|
+
// GET /v1/agent/permissions returns a deterministic `version` hash over the
|
|
12
|
+
// effective spec (scopes + requireApproval + connections). We poll it with
|
|
13
|
+
// If-None-Match, so an unchanged spec costs a 304 with no body. When the hash
|
|
14
|
+
// does move we emit notifications/tools/list_changed; the host re-reads
|
|
15
|
+
// tools/list, and because the list_platforms description is stamped with the
|
|
16
|
+
// current version (see index.ts), the description the model sees actually
|
|
17
|
+
// differs — which is what gets the stale cache invalidated in practice. A bare
|
|
18
|
+
// list_changed on an otherwise-identical tool list would be a no-op.
|
|
19
|
+
//
|
|
20
|
+
// Note this only works on a long-lived transport. The in-proxy MCP endpoint
|
|
21
|
+
// (apps/proxy/src/routes/mcp.ts) is deliberately stateless — a fresh Server per
|
|
22
|
+
// request — so it has no server→client channel and cannot push at all; there,
|
|
23
|
+
// cheap revalidation via the 304 path is the whole story.
|
|
24
|
+
import { signJWT } from "./auth.js";
|
|
25
|
+
import { fetchWithTimeout } from "./net.js";
|
|
26
|
+
const DEFAULT_POLL_MS = 60_000;
|
|
27
|
+
// Floor so a typo in the env var can't turn an agent into a polling hammer
|
|
28
|
+
// against the proxy.
|
|
29
|
+
const MIN_POLL_MS = 10_000;
|
|
30
|
+
let currentVersion = null;
|
|
31
|
+
let timer = null;
|
|
32
|
+
export function getPermissionsVersion() {
|
|
33
|
+
return currentVersion;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record a version observed elsewhere (a normal list_platforms tool call).
|
|
37
|
+
* Returns true when this is a CHANGE from a previously-known version — the
|
|
38
|
+
* first observation only establishes the baseline and is not a change.
|
|
39
|
+
*/
|
|
40
|
+
export function recordPermissionsVersion(version) {
|
|
41
|
+
if (!version || typeof version !== "string")
|
|
42
|
+
return false;
|
|
43
|
+
const hadBaseline = currentVersion !== null;
|
|
44
|
+
const changed = hadBaseline && version !== currentVersion;
|
|
45
|
+
currentVersion = version;
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
|
48
|
+
/** Test seam: forget everything the watcher has learned. */
|
|
49
|
+
export function resetPermissionsVersion() {
|
|
50
|
+
currentVersion = null;
|
|
51
|
+
}
|
|
52
|
+
export function pollIntervalMs(raw = process.env.AV_PERMISSIONS_POLL_MS) {
|
|
53
|
+
if (raw === undefined || raw === "")
|
|
54
|
+
return DEFAULT_POLL_MS;
|
|
55
|
+
const parsed = Number(raw);
|
|
56
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
57
|
+
return 0; // 0 / invalid ⇒ disabled
|
|
58
|
+
return Math.max(parsed, MIN_POLL_MS);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One revalidation round-trip. Exported for tests. Returns true when the spec
|
|
62
|
+
* changed (and a notification was emitted).
|
|
63
|
+
*/
|
|
64
|
+
export async function checkPermissionsVersion(ctx) {
|
|
65
|
+
if (ctx.AGENT_PRIVATE_KEY_RAW === null)
|
|
66
|
+
return false;
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
const token = await signJWT(ctx);
|
|
70
|
+
const headers = {
|
|
71
|
+
Authorization: `Bearer ${token}`,
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
};
|
|
74
|
+
if (currentVersion)
|
|
75
|
+
headers["If-None-Match"] = `"${currentVersion}"`;
|
|
76
|
+
response = await fetchWithTimeout(`${ctx.PROXY_URL}/v1/agent/permissions`, { method: "GET", headers }, 10_000);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false; // transient — try again next tick
|
|
80
|
+
}
|
|
81
|
+
// Unchanged: the whole point of the conditional request.
|
|
82
|
+
if (response.status === 304)
|
|
83
|
+
return false;
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
return false;
|
|
86
|
+
let version;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = (await response.json());
|
|
89
|
+
const inner = (parsed && typeof parsed === "object" && "data" in parsed
|
|
90
|
+
? parsed.data
|
|
91
|
+
: parsed);
|
|
92
|
+
if (inner && typeof inner.version === "string")
|
|
93
|
+
version = inner.version;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!recordPermissionsVersion(version))
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
await ctx.server.notification({ method: "notifications/tools/list_changed" });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Best-effort: a host that doesn't accept the notification still gets the
|
|
105
|
+
// corrected spec the next time it calls list_platforms.
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Start background revalidation. No-op without credentials or when polling is
|
|
111
|
+
* disabled. The timer is unref'd so it can never hold the process open.
|
|
112
|
+
*/
|
|
113
|
+
export function startPermissionsWatcher(ctx) {
|
|
114
|
+
if (timer)
|
|
115
|
+
return;
|
|
116
|
+
if (ctx.AGENT_PRIVATE_KEY_RAW === null)
|
|
117
|
+
return;
|
|
118
|
+
const interval = pollIntervalMs();
|
|
119
|
+
if (interval === 0)
|
|
120
|
+
return;
|
|
121
|
+
timer = setInterval(() => {
|
|
122
|
+
void checkPermissionsVersion(ctx).catch(() => { });
|
|
123
|
+
}, interval);
|
|
124
|
+
// `unref` is absent in some non-Node runtimes; guard rather than assume.
|
|
125
|
+
timer.unref?.();
|
|
126
|
+
}
|
|
127
|
+
export function stopPermissionsWatcher() {
|
|
128
|
+
if (!timer)
|
|
129
|
+
return;
|
|
130
|
+
clearInterval(timer);
|
|
131
|
+
timer = null;
|
|
132
|
+
}
|
package/dist/tools/handlers.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// module globals. Behavior is unchanged — pinned by test/introspection.test.ts.
|
|
7
7
|
import { errorContent, jsonContent, tryParseJson, safeJsonParse, sleep, diagnoseNetworkError, fetchWithTimeout, } from "../net.js";
|
|
8
8
|
import { requireCredentials, fetchWithAuth } from "../auth.js";
|
|
9
|
+
import { recordPermissionsVersion } from "../permissions-watcher.js";
|
|
9
10
|
export async function handleListPlatforms(ctx) {
|
|
10
11
|
const gate = await requireCredentials(ctx);
|
|
11
12
|
if (gate)
|
|
@@ -29,6 +30,9 @@ export async function handleListPlatforms(ctx) {
|
|
|
29
30
|
? parsed.data
|
|
30
31
|
: parsed;
|
|
31
32
|
if (inner && typeof inner === "object") {
|
|
33
|
+
// Keep the watcher's baseline in step with what the agent was just told, so
|
|
34
|
+
// a manual refresh doesn't trigger a spurious "changed" notification.
|
|
35
|
+
recordPermissionsVersion(inner.version);
|
|
32
36
|
return {
|
|
33
37
|
content: [{ type: "text", text: body }],
|
|
34
38
|
structuredContent: inner,
|
|
@@ -91,6 +95,9 @@ export async function handleUsePlatform(ctx, params, progressToken) {
|
|
|
91
95
|
scope: params.scope,
|
|
92
96
|
...(params.data !== undefined && { data: params.data }),
|
|
93
97
|
...(params.connection_id ? { connection_id: params.connection_id } : {}),
|
|
98
|
+
// Agent-supplied justification. Forwarded verbatim; the proxy normalizes
|
|
99
|
+
// and caps it before it reaches the approver's screen.
|
|
100
|
+
...(params.reason ? { reason: params.reason } : {}),
|
|
94
101
|
};
|
|
95
102
|
let response;
|
|
96
103
|
try {
|
package/dist/tools/schemas.js
CHANGED
|
@@ -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
|
|
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.",
|
|
11
11
|
inputSchema: { type: "object", properties: {} },
|
|
12
12
|
outputSchema: {
|
|
13
13
|
type: "object",
|
|
@@ -46,7 +46,7 @@ export const LIST_PLATFORMS_TOOL = {
|
|
|
46
46
|
};
|
|
47
47
|
export const USE_PLATFORM_TOOL = {
|
|
48
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.",
|
|
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), reason (string, optional — why you are making this call; shown to the human approver when the action is gated).\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
50
|
inputSchema: {
|
|
51
51
|
type: "object",
|
|
52
52
|
properties: {
|
|
@@ -79,6 +79,10 @@ export const USE_PLATFORM_TOOL = {
|
|
|
79
79
|
type: "string",
|
|
80
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
81
|
},
|
|
82
|
+
reason: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Optional but strongly encouraged. One sentence on WHY you are making this call. If the action requires owner approval, this is shown to the human who decides — without it their approval is a blind yes/no. Max 500 characters (longer is truncated). Plain text; markup is not rendered.",
|
|
85
|
+
},
|
|
82
86
|
},
|
|
83
87
|
required: ["platform", "endpoint", "method", "scope"],
|
|
84
88
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvalet/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "AgentValet MCP server — lets AI agents call approved platforms via the AgentValet proxy",
|
|
5
|
+
"mcpName": "io.github.agentvalet/mcp-server",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"bin": {
|
|
@@ -11,14 +12,6 @@
|
|
|
11
12
|
"dist",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"dev": "tsx src/index.ts",
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"start": "node dist/index.js",
|
|
18
|
-
"test": "vitest run",
|
|
19
|
-
"typecheck": "tsc --noEmit",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
15
|
"dependencies": {
|
|
23
16
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
17
|
"jose": "^5.10.0"
|
|
@@ -34,8 +27,8 @@
|
|
|
34
27
|
},
|
|
35
28
|
"repository": {
|
|
36
29
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
38
|
-
"directory": "
|
|
30
|
+
"url": "git+https://github.com/AgentValet/AgentValet.git",
|
|
31
|
+
"directory": "packages/mcp-server"
|
|
39
32
|
},
|
|
40
33
|
"keywords": [
|
|
41
34
|
"ai",
|
|
@@ -43,5 +36,13 @@
|
|
|
43
36
|
"mcp",
|
|
44
37
|
"agentvalet"
|
|
45
38
|
],
|
|
46
|
-
"license": "MIT"
|
|
47
|
-
|
|
39
|
+
"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
|
+
}
|