@agentvalet/mcp-server 0.1.1 → 0.2.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 ADDED
@@ -0,0 +1,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 |
53
+ | `agent_register` | None | Self-register a new agent with an owner |
54
+ | `agent_status` | None | Poll registration approval status |
55
+ | `authzen_evaluate` | None | Check if this agent has access to a platform scope |
56
+
57
+ ## Transports
58
+
59
+ **STDIO (default)** — compatible with all MCP hosts (Claude Code, Cursor, Codex CLI).
60
+
61
+ **HTTP** — set `MCP_TRANSPORT=http` and optionally `MCP_PORT=3100`. Uses `StreamableHTTPServerTransport`.
62
+
63
+ ## Architecture
64
+
65
+ 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.
66
+
67
+ 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/config.js ADDED
@@ -0,0 +1,34 @@
1
+ import { readPrivateKeyFromEnv } from "./pem.js";
2
+ import { importPKCS8 } from "jose";
3
+ export async function validateConfig() {
4
+ const missing = [];
5
+ for (const key of ["AGENT_ID", "OWNER_ID", "PROXY_URL"]) {
6
+ if (!process.env[key])
7
+ missing.push(key);
8
+ }
9
+ if (missing.length > 0) {
10
+ process.stderr.write(`[mcp-server] Missing required environment variables: ${missing.join(", ")}\n`);
11
+ process.exit(1);
12
+ }
13
+ const agentId = process.env.AGENT_ID;
14
+ const ownerId = process.env.OWNER_ID;
15
+ const proxyUrl = process.env.PROXY_URL.replace(/\/$/, "");
16
+ let privateKeyPem = null;
17
+ let privateKey = null;
18
+ try {
19
+ privateKeyPem = readPrivateKeyFromEnv();
20
+ }
21
+ catch {
22
+ // No key provided — tools will return pending-activation response
23
+ }
24
+ if (privateKeyPem !== null) {
25
+ try {
26
+ privateKey = await importPKCS8(privateKeyPem, "RS256");
27
+ }
28
+ catch (err) {
29
+ process.stderr.write(`[mcp-server] Invalid private key: ${err instanceof Error ? err.message : err}\n`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+ return { agentId, ownerId, proxyUrl, privateKeyPem, privateKey };
34
+ }
package/dist/index.js CHANGED
@@ -1,48 +1,12 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
- import { SignJWT, importPKCS8 } from "jose";
5
- import { readFileSync } from "fs";
4
+ import { SignJWT } from "jose";
5
+ import { validateConfig } from "./config.js";
6
6
  // ---------------------------------------------------------------------------
7
7
  // Startup env validation
8
8
  // ---------------------------------------------------------------------------
9
- // Resolve AGENT_PRIVATE_KEY: accept raw key content or a path via AGENT_PRIVATE_KEY_PATH
10
- if (!process.env.AGENT_PRIVATE_KEY && process.env.AGENT_PRIVATE_KEY_PATH) {
11
- try {
12
- process.env.AGENT_PRIVATE_KEY = readFileSync(process.env.AGENT_PRIVATE_KEY_PATH, "utf-8").trim();
13
- }
14
- catch (err) {
15
- process.stderr.write(`[mcp-server] Cannot read AGENT_PRIVATE_KEY_PATH: ${err instanceof Error ? err.message : err}\n`);
16
- process.exit(1);
17
- }
18
- }
19
- const REQUIRED_ENV = ["AGENT_PRIVATE_KEY", "AGENT_ID", "OWNER_ID", "PROXY_URL"];
20
- for (const key of REQUIRED_ENV) {
21
- if (!process.env[key]) {
22
- process.stderr.write(`[mcp-server] Missing required environment variable: ${key} (or AGENT_PRIVATE_KEY_PATH)\n`);
23
- process.exit(1);
24
- }
25
- }
26
- const AGENT_PRIVATE_KEY_RAW = process.env.AGENT_PRIVATE_KEY;
27
- const AGENT_ID = process.env.AGENT_ID;
28
- const OWNER_ID = process.env.OWNER_ID;
29
- const PROXY_URL = process.env.PROXY_URL.replace(/\/$/, "");
30
- // If the key is the sentinel value, defer key import — the guard in each tool
31
- // handler will return a user-friendly error before any signing attempt.
32
- let privateKey = null;
33
- if (AGENT_PRIVATE_KEY_RAW !== "PENDING_FIRST_CALL") {
34
- const rawKey = AGENT_PRIVATE_KEY_RAW.trim();
35
- const AGENT_PRIVATE_KEY = rawKey.startsWith("-----")
36
- ? rawKey.replace(/\\n/g, "\n")
37
- : `-----BEGIN PRIVATE KEY-----\n${rawKey}\n-----END PRIVATE KEY-----`;
38
- try {
39
- privateKey = await importPKCS8(AGENT_PRIVATE_KEY, "RS256");
40
- }
41
- catch (err) {
42
- process.stderr.write(`[mcp-server] Invalid AGENT_PRIVATE_KEY: ${err instanceof Error ? err.message : err}\n`);
43
- process.exit(1);
44
- }
45
- }
9
+ const { agentId: AGENT_ID, ownerId: OWNER_ID, proxyUrl: PROXY_URL, privateKeyPem: AGENT_PRIVATE_KEY_RAW, privateKey } = await validateConfig();
46
10
  // ---------------------------------------------------------------------------
47
11
  // JWT signing
48
12
  // ---------------------------------------------------------------------------
@@ -162,11 +126,6 @@ const AGENT_STATUS_TOOL = {
162
126
  required: ["token"],
163
127
  },
164
128
  };
165
- const AGENT_PERMISSIONS_TOOL = {
166
- name: "agent_permissions",
167
- description: "agent_permissions: List all platforms and permission scopes granted to this agent.\nInput: None.\nReturns: platforms array with platformId, platformName, scopes, requireApproval.\nAuth: Bearer JWT.",
168
- inputSchema: { type: "object", properties: {} },
169
- };
170
129
  const AUTHZEN_EVALUATE_TOOL = {
171
130
  name: "authzen_evaluate",
172
131
  description: "authzen_evaluate: Evaluate whether this agent has access to a specific platform scope.\nInput: platform_id (string), scope (string).\nReturns: decision (boolean), reason (\"approved\"|\"denied\"|\"revoked\"|\"scope_not_granted\").\nAuth: None.",
@@ -185,24 +144,7 @@ const AUTHZEN_EVALUATE_TOOL = {
185
144
  required: ["platform_id", "scope"],
186
145
  },
187
146
  };
188
- const INTENT_RESOLVE_TOOL = {
189
- name: "intent_resolve",
190
- description: "intent_resolve: Resolve a natural-language intent to the exact AgentValet API action to call.\nInput: intent (string, required), context (object, optional).\nReturns: resolved (boolean), capability object with endpoint, auth_required, example_request, or suggestions array.\nAuth: None.",
191
- inputSchema: {
192
- type: "object",
193
- properties: {
194
- intent: {
195
- type: "string",
196
- description: "Natural-language description of what the agent wants to do",
197
- },
198
- context: {
199
- type: "object",
200
- description: "Optional context object to aid resolution",
201
- },
202
- },
203
- required: ["intent"],
204
- },
205
- };
147
+ // TODO: intent_resolve tool — planned for future release
206
148
  // ---------------------------------------------------------------------------
207
149
  // MCP server setup
208
150
  // ---------------------------------------------------------------------------
@@ -213,9 +155,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
213
155
  USE_PLATFORM_TOOL,
214
156
  AGENT_REGISTER_TOOL,
215
157
  AGENT_STATUS_TOOL,
216
- AGENT_PERMISSIONS_TOOL,
217
158
  AUTHZEN_EVALUATE_TOOL,
218
- INTENT_RESOLVE_TOOL,
219
159
  ],
220
160
  }));
221
161
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -260,21 +200,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
200
  }
261
201
  return await handleAgentStatus(args.token);
262
202
  }
263
- if (name === "agent_permissions") {
264
- return await handleAgentPermissions();
265
- }
266
203
  if (name === "authzen_evaluate") {
267
204
  if (!args || typeof args.platform_id !== "string" || typeof args.scope !== "string") {
268
205
  return errorContent("Invalid or missing arguments: platform_id and scope are required");
269
206
  }
270
207
  return await handleAuthzenEvaluate(args.platform_id, args.scope);
271
208
  }
272
- if (name === "intent_resolve") {
273
- if (!args || typeof args.intent !== "string") {
274
- return errorContent("Invalid or missing argument: intent is required");
275
- }
276
- return await handleIntentResolve(args.intent, args.context);
277
- }
278
209
  return {
279
210
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
280
211
  isError: true,
@@ -288,12 +219,20 @@ function fetchWithTimeout(url, init, timeoutMs = 15_000) {
288
219
  const timer = setTimeout(() => ac.abort(), timeoutMs);
289
220
  return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
290
221
  }
291
- async function bearerFetch(url, init) {
292
- const token = await signJWT();
293
- return fetchWithTimeout(url, {
294
- ...init,
295
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init.headers },
296
- });
222
+ async function fetchWithAuth(url, init) {
223
+ const makeRequest = async () => {
224
+ const token = await signJWT();
225
+ return fetchWithTimeout(url, {
226
+ ...init,
227
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init.headers },
228
+ });
229
+ };
230
+ const response = await makeRequest();
231
+ // Retry once on 401 — the JWT may have been issued just before clock skew threshold
232
+ if (response.status === 401) {
233
+ return makeRequest();
234
+ }
235
+ return response;
297
236
  }
298
237
  function errorContent(message) {
299
238
  return { content: [{ type: "text", text: message }], isError: true };
@@ -302,13 +241,13 @@ function errorContent(message) {
302
241
  // Tool handlers
303
242
  // ---------------------------------------------------------------------------
304
243
  async function handleListPlatforms() {
305
- if (AGENT_PRIVATE_KEY_RAW === "PENDING_FIRST_CALL") {
244
+ if (AGENT_PRIVATE_KEY_RAW === null) {
306
245
  await notifyBindSecret();
307
246
  return pendingFirstCallResponse();
308
247
  }
309
248
  let response;
310
249
  try {
311
- response = await bearerFetch(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
250
+ response = await fetchWithAuth(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
312
251
  }
313
252
  catch (err) {
314
253
  return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
@@ -319,7 +258,7 @@ async function handleListPlatforms() {
319
258
  return { content: [{ type: "text", text: body }] };
320
259
  }
321
260
  async function handleUsePlatform(params) {
322
- if (AGENT_PRIVATE_KEY_RAW === "PENDING_FIRST_CALL") {
261
+ if (AGENT_PRIVATE_KEY_RAW === null) {
323
262
  await notifyBindSecret();
324
263
  return pendingFirstCallResponse();
325
264
  }
@@ -332,7 +271,7 @@ async function handleUsePlatform(params) {
332
271
  };
333
272
  let response;
334
273
  try {
335
- response = await bearerFetch(`${PROXY_URL}/v1/actions`, {
274
+ response = await fetchWithAuth(`${PROXY_URL}/v1/actions`, {
336
275
  method: "POST",
337
276
  body: JSON.stringify(requestBody),
338
277
  });
@@ -378,23 +317,6 @@ async function handleAgentStatus(token) {
378
317
  return errorContent(`Proxy error ${response.status}: ${body}`);
379
318
  return { content: [{ type: "text", text: body }] };
380
319
  }
381
- async function handleAgentPermissions() {
382
- if (AGENT_PRIVATE_KEY_RAW === "PENDING_FIRST_CALL") {
383
- await notifyBindSecret();
384
- return pendingFirstCallResponse();
385
- }
386
- let response;
387
- try {
388
- response = await bearerFetch(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
389
- }
390
- catch (err) {
391
- return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
392
- }
393
- const body = await response.text();
394
- if (!response.ok)
395
- return errorContent(`Proxy error ${response.status}: ${body}`);
396
- return { content: [{ type: "text", text: body }] };
397
- }
398
320
  async function handleAuthzenEvaluate(platformId, scope) {
399
321
  const authzenBody = {
400
322
  subject: { type: "agent", id: AGENT_ID },
@@ -417,28 +339,15 @@ async function handleAuthzenEvaluate(platformId, scope) {
417
339
  return errorContent(`Proxy error ${response.status}: ${body}`);
418
340
  return { content: [{ type: "text", text: body }] };
419
341
  }
420
- async function handleIntentResolve(intent, context) {
421
- const requestBody = { intent };
422
- if (context !== undefined)
423
- requestBody.context = context;
424
- let response;
425
- try {
426
- response = await fetchWithTimeout(`${PROXY_URL}/v1/intent/resolve`, {
427
- method: "POST",
428
- headers: { "Content-Type": "application/json" },
429
- body: JSON.stringify(requestBody),
430
- });
431
- }
432
- catch (err) {
433
- return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
434
- }
435
- const body = await response.text();
436
- if (!response.ok)
437
- return errorContent(`Proxy error ${response.status}: ${body}`);
438
- return { content: [{ type: "text", text: body }] };
439
- }
440
342
  // ---------------------------------------------------------------------------
441
343
  // Connect transport
442
344
  // ---------------------------------------------------------------------------
443
- const transport = new StdioServerTransport();
444
- await server.connect(transport);
345
+ if (process.env.MCP_TRANSPORT === "http") {
346
+ const { startHttpTransport } = await import("./transports/http.js");
347
+ const port = parseInt(process.env.MCP_PORT ?? "3100", 10);
348
+ await startHttpTransport(server, port);
349
+ }
350
+ else {
351
+ const transport = new StdioServerTransport();
352
+ await server.connect(transport);
353
+ }
package/dist/pem.js ADDED
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "fs";
2
+ /**
3
+ * Reads the RS256 private key from environment variables, supporting four formats:
4
+ * - AGENT_PRIVATE_KEY_B64: base64-encoded PEM
5
+ * - AGENT_PRIVATE_KEY_PATH: path to a PEM file
6
+ * - AGENT_PRIVATE_KEY: raw multi-line PEM or \n-escaped single-line PEM
7
+ */
8
+ export function readPrivateKeyFromEnv() {
9
+ // 1. Base64-encoded PEM
10
+ if (process.env.AGENT_PRIVATE_KEY_B64) {
11
+ return Buffer.from(process.env.AGENT_PRIVATE_KEY_B64, "base64").toString("utf-8").trim();
12
+ }
13
+ // 2. Path to PEM file
14
+ if (process.env.AGENT_PRIVATE_KEY_PATH) {
15
+ try {
16
+ return readFileSync(process.env.AGENT_PRIVATE_KEY_PATH, "utf-8").trim();
17
+ }
18
+ catch (err) {
19
+ throw new Error(`Cannot read AGENT_PRIVATE_KEY_PATH: ${err instanceof Error ? err.message : err}`);
20
+ }
21
+ }
22
+ // 3. Raw PEM content (multi-line or \n-escaped)
23
+ if (process.env.AGENT_PRIVATE_KEY) {
24
+ const raw = process.env.AGENT_PRIVATE_KEY.trim();
25
+ // Unescape \n sequences
26
+ const unescaped = raw.replace(/\\n/g, "\n");
27
+ // Wrap bare base64 blob without headers
28
+ if (!unescaped.startsWith("-----")) {
29
+ return `-----BEGIN PRIVATE KEY-----\n${unescaped}\n-----END PRIVATE KEY-----`;
30
+ }
31
+ return unescaped;
32
+ }
33
+ throw new Error("No private key provided. Set AGENT_PRIVATE_KEY, AGENT_PRIVATE_KEY_PATH, or AGENT_PRIVATE_KEY_B64.");
34
+ }
@@ -0,0 +1,38 @@
1
+ import { createServer } from "node:http";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ export async function startHttpTransport(server, port) {
4
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
5
+ const httpServer = createServer(async (req, res) => {
6
+ try {
7
+ await transport.handleRequest(req, res, await readBody(req));
8
+ }
9
+ catch (err) {
10
+ res.writeHead(500);
11
+ res.end(JSON.stringify({ error: "Internal server error" }));
12
+ }
13
+ });
14
+ await server.connect(transport);
15
+ httpServer.listen(port, () => {
16
+ process.stderr.write(`[mcp-server] HTTP transport listening on port ${port}\n`);
17
+ });
18
+ }
19
+ function readBody(req) {
20
+ return new Promise((resolve, reject) => {
21
+ const chunks = [];
22
+ req.on("data", (chunk) => chunks.push(chunk));
23
+ req.on("end", () => {
24
+ const raw = Buffer.concat(chunks).toString("utf-8");
25
+ if (!raw) {
26
+ resolve(undefined);
27
+ return;
28
+ }
29
+ try {
30
+ resolve(JSON.parse(raw));
31
+ }
32
+ catch {
33
+ resolve(undefined);
34
+ }
35
+ });
36
+ req.on("error", reject);
37
+ });
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "AgentValet MCP server — lets AI agents call approved platforms via the AgentValet proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",