@agentvalet/mcp-server 1.10.0 → 1.12.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/index.js +5 -1
- package/dist/permissions-watcher.js +86 -1
- package/dist/tools/schemas.js +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { signJWT } from "./auth.js";
|
|
|
25
25
|
import { ALLOWED_METHODS, ALL_TOOLS } from "./tools/schemas.js";
|
|
26
26
|
import { handleListPlatforms, handleUsePlatform, handleAgentRegister, handleAgentStatus, handleAuthzenEvaluate, handleListMyPendingActions, handleReportSelfDiagnostic, handleRequestPlatformAccess, handleIssueChildAgent, } from "./tools/handlers.js";
|
|
27
27
|
import { errorContent } from "./net.js";
|
|
28
|
-
import { getPermissionsVersion, recordPermissionsVersion, startPermissionsWatcher, } from "./permissions-watcher.js";
|
|
28
|
+
import { getPermissionsVersion, noteAgentActivity, recordPermissionsVersion, startPermissionsWatcher, } from "./permissions-watcher.js";
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Startup env validation
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
@@ -117,6 +117,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
117
117
|
});
|
|
118
118
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
119
119
|
const { name, arguments: args } = request.params;
|
|
120
|
+
// The agent is doing something, so its view of the spec is about to matter.
|
|
121
|
+
// This is what un-idles the permissions watcher (and resets its backoff); an
|
|
122
|
+
// editor window nobody is using never reaches here and never revalidates.
|
|
123
|
+
noteAgentActivity();
|
|
120
124
|
if (name === "list_platforms") {
|
|
121
125
|
return await handleListPlatforms(ctx);
|
|
122
126
|
}
|
|
@@ -22,14 +22,66 @@
|
|
|
22
22
|
// (apps/proxy/src/routes/mcp.ts) is deliberately stateless — a fresh Server per
|
|
23
23
|
// request — so it has no server→client channel and cannot push at all; there,
|
|
24
24
|
// cheap revalidation via the 304 path is the whole story.
|
|
25
|
+
//
|
|
26
|
+
// WHEN we revalidate, and why it is not simply "every 60 seconds":
|
|
27
|
+
//
|
|
28
|
+
// A fixed timer polls a resource that almost never changes, on behalf of a
|
|
29
|
+
// session that is usually doing nothing. On 2026-09-01 that shape turned 210
|
|
30
|
+
// real governed calls into ~7,000 revalidations in a day — mostly from editor
|
|
31
|
+
// windows left open overnight — and each one cost the proxy a full spec
|
|
32
|
+
// resolution. The spec only matters when the agent is about to ACT on it, so
|
|
33
|
+
// the timer now defers to two signals:
|
|
34
|
+
//
|
|
35
|
+
// 1. ACTIVITY. noteAgentActivity() is called from the tool dispatch on every
|
|
36
|
+
// tool call. A server that has not served one in IDLE_AFTER_MS stops
|
|
37
|
+
// revalidating entirely — an idle window costs nothing — and resumes the
|
|
38
|
+
// moment the agent does anything.
|
|
39
|
+
// 2. STABILITY. Each consecutive 304 backs the interval off (doubling, to a
|
|
40
|
+
// cap). A spec that has not moved in twenty minutes is not about to be
|
|
41
|
+
// checked every minute. Any observed change, and any tool call, resets it.
|
|
42
|
+
//
|
|
43
|
+
// What this trades away: after an idle stretch, the FIRST tool call may still
|
|
44
|
+
// reason from a spec up to one poll old. That is the pre-watcher behaviour and
|
|
45
|
+
// it is safe by construction — the proxy authorises every call against the live
|
|
46
|
+
// database regardless, so the worst case is one clean 403 rather than a wrongly
|
|
47
|
+
// permitted action. An agent that wants certainty calls list_platforms, which
|
|
48
|
+
// revalidates synchronously.
|
|
25
49
|
import { signJWT } from "./auth.js";
|
|
26
50
|
import { fetchWithTimeout } from "./net.js";
|
|
27
51
|
const DEFAULT_POLL_MS = 60_000;
|
|
28
52
|
// Floor so a typo in the env var can't turn an agent into a polling hammer
|
|
29
53
|
// against the proxy.
|
|
30
54
|
const MIN_POLL_MS = 10_000;
|
|
55
|
+
// No tool call in this long ⇒ nobody is relying on the spec, so stop asking for
|
|
56
|
+
// it. Generous on purpose: the point is to catch the window left open all
|
|
57
|
+
// night, not to punish a user who paused to read something.
|
|
58
|
+
const IDLE_AFTER_MS = 15 * 60 * 1000;
|
|
59
|
+
// Ceiling on the stability backoff, counted in base intervals. At the 60s
|
|
60
|
+
// default this settles an unchanging spec at one revalidation every ~16 min.
|
|
61
|
+
const MAX_BACKOFF_TICKS = 15;
|
|
31
62
|
let currentVersion = null;
|
|
32
63
|
let timer = null;
|
|
64
|
+
// Boot counts as activity: a server that has just started is about to be used.
|
|
65
|
+
let lastActivityAt = Date.now();
|
|
66
|
+
// Base intervals still to be skipped before the next revalidation.
|
|
67
|
+
let ticksToSkip = 0;
|
|
68
|
+
// Consecutive revalidations that found nothing changed; drives the backoff.
|
|
69
|
+
let unchangedStreak = 0;
|
|
70
|
+
/**
|
|
71
|
+
* Record that the agent just did something. Called from the tool dispatch on
|
|
72
|
+
* EVERY tool call — the cheapest possible signal that the spec is about to
|
|
73
|
+
* matter again. Resets both gates, so a session waking up revalidates on the
|
|
74
|
+
* next tick rather than at the end of a long backoff.
|
|
75
|
+
*/
|
|
76
|
+
export function noteAgentActivity() {
|
|
77
|
+
lastActivityAt = Date.now();
|
|
78
|
+
ticksToSkip = 0;
|
|
79
|
+
unchangedStreak = 0;
|
|
80
|
+
}
|
|
81
|
+
/** Whether the watcher would skip a tick right now. Exported for tests. */
|
|
82
|
+
export function isIdle(now = Date.now()) {
|
|
83
|
+
return now - lastActivityAt > IDLE_AFTER_MS;
|
|
84
|
+
}
|
|
33
85
|
export function getPermissionsVersion() {
|
|
34
86
|
return currentVersion;
|
|
35
87
|
}
|
|
@@ -49,6 +101,9 @@ export function recordPermissionsVersion(version) {
|
|
|
49
101
|
/** Test seam: forget everything the watcher has learned. */
|
|
50
102
|
export function resetPermissionsVersion() {
|
|
51
103
|
currentVersion = null;
|
|
104
|
+
lastActivityAt = Date.now();
|
|
105
|
+
ticksToSkip = 0;
|
|
106
|
+
unchangedStreak = 0;
|
|
52
107
|
}
|
|
53
108
|
export function pollIntervalMs(raw = process.env.AV_PERMISSIONS_POLL_MS) {
|
|
54
109
|
if (raw === undefined || raw === "")
|
|
@@ -107,6 +162,36 @@ export async function checkPermissionsVersion(ctx) {
|
|
|
107
162
|
}
|
|
108
163
|
return true;
|
|
109
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* One timer tick: decide whether this is a moment worth spending a request on,
|
|
167
|
+
* and revalidate if so. Exported for tests. Returns true when it actually went
|
|
168
|
+
* to the network.
|
|
169
|
+
*/
|
|
170
|
+
export async function tick(ctx) {
|
|
171
|
+
// Nobody is using this session. The spec can drift freely — the next tool
|
|
172
|
+
// call resets the gate, and the proxy authorises against the live database
|
|
173
|
+
// whatever this server happens to believe.
|
|
174
|
+
if (isIdle())
|
|
175
|
+
return false;
|
|
176
|
+
// Backing off after a run of unchanged reads.
|
|
177
|
+
if (ticksToSkip > 0) {
|
|
178
|
+
ticksToSkip -= 1;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const changed = await checkPermissionsVersion(ctx);
|
|
182
|
+
if (changed) {
|
|
183
|
+
// Something moved. Watch closely again — an owner mid-edit often makes
|
|
184
|
+
// several changes in a row.
|
|
185
|
+
unchangedStreak = 0;
|
|
186
|
+
ticksToSkip = 0;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// 1, 3, 7, 15, 15, … base intervals of silence.
|
|
190
|
+
unchangedStreak += 1;
|
|
191
|
+
ticksToSkip = Math.min(2 ** unchangedStreak - 1, MAX_BACKOFF_TICKS);
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
110
195
|
/**
|
|
111
196
|
* Start background revalidation. No-op without credentials or when polling is
|
|
112
197
|
* disabled. The timer is unref'd so it can never hold the process open.
|
|
@@ -120,7 +205,7 @@ export function startPermissionsWatcher(ctx) {
|
|
|
120
205
|
if (interval === 0)
|
|
121
206
|
return;
|
|
122
207
|
timer = setInterval(() => {
|
|
123
|
-
void
|
|
208
|
+
void tick(ctx).catch(() => { });
|
|
124
209
|
}, interval);
|
|
125
210
|
// `unref` is absent in some non-Node runtimes; guard rather than assume.
|
|
126
211
|
timer.unref?.();
|
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, 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.",
|
|
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?, callStyle? }], 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).\nCall style: `callStyle` is present only as \"graphql\", and says how to ADDRESS the platform rather than what you may do there: that platform has ONE endpoint taking {query, variables} via POST, so pass \"/\" as the endpoint and put the operation in the body. Its path and HTTP method carry no meaning, read_platform cannot be used on it at all, and a GET returns an upstream error naming neither the platform nor the cause. Absent means a conventional REST API.\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",
|
|
@@ -101,7 +101,7 @@ export const USE_PLATFORM_TOOL = {
|
|
|
101
101
|
// readOnlyHint annotation enforced rather than merely promised.
|
|
102
102
|
export const READ_PLATFORM_TOOL = {
|
|
103
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.",
|
|
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. A platform that list_platforms reports as callStyle 'graphql' cannot be read with this tool at all: it exposes a SINGLE endpoint that accepts only POST {query, variables}, so send the read query through write_platform (or use_platform with method POST) even though it is a read. Issuing a GET there returns an upstream error that names neither the platform nor the cause — it is not a broker fault. 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
105
|
inputSchema: {
|
|
106
106
|
type: "object",
|
|
107
107
|
properties: {
|
|
@@ -122,7 +122,7 @@ export const READ_PLATFORM_TOOL = {
|
|
|
122
122
|
};
|
|
123
123
|
export const WRITE_PLATFORM_TOOL = {
|
|
124
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.",
|
|
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. For a platform whose callStyle is 'graphql', pass \"/\" as the endpoint and put the operation in body as {query, variables}: the path and the HTTP method carry no meaning on that platform. 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
126
|
inputSchema: {
|
|
127
127
|
type: "object",
|
|
128
128
|
properties: {
|
|
@@ -153,7 +153,7 @@ export const WRITE_PLATFORM_TOOL = {
|
|
|
153
153
|
};
|
|
154
154
|
export const DELETE_PLATFORM_TOOL = {
|
|
155
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.",
|
|
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. For a platform whose callStyle is 'graphql', pass \"/\" as the endpoint and put the operation in body as {query, variables}: the path and the HTTP method carry no meaning on that platform. 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
157
|
inputSchema: {
|
|
158
158
|
type: "object",
|
|
159
159
|
properties: {
|
package/package.json
CHANGED