@agentvalet/mcp-server 0.3.6 → 0.3.9
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/bind.js +131 -0
- package/dist/config.js +73 -9
- package/dist/index.js +66 -8
- package/dist/pem.js +13 -1
- package/package.json +1 -1
package/dist/bind.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// First-run invite bind for AgentValet MCP server.
|
|
2
|
+
//
|
|
3
|
+
// Triggered when INVITE_BIND_SECRET is set in env and no agent identity
|
|
4
|
+
// (AGENT_ID + private key) is yet available. We generate an RSA-2048
|
|
5
|
+
// keypair locally, POST the public key + bind_secret to /v1/invites/bind,
|
|
6
|
+
// and persist the returned identity + private key to ~/.agentvalet/.
|
|
7
|
+
//
|
|
8
|
+
// The bind_secret is one-shot, with a 30-minute TTL — the proxy returns
|
|
9
|
+
// 410 on replay. We DO NOT retry, because a 410 either means the secret
|
|
10
|
+
// was already consumed by an earlier run (so the identity should already
|
|
11
|
+
// be on disk) or it expired (the invitee needs a re-issue).
|
|
12
|
+
//
|
|
13
|
+
// Persistence layout (created with permission 0700 on the parent dir):
|
|
14
|
+
// ~/.agentvalet/agent.key PEM-encoded private key, mode 0600
|
|
15
|
+
// ~/.agentvalet/agent.json { agent_id, owner_id, proxy_url, bound_at }
|
|
16
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
17
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
const AGENTVALET_DIR = join(homedir(), ".agentvalet");
|
|
21
|
+
const KEY_PATH = join(AGENTVALET_DIR, "agent.key");
|
|
22
|
+
const IDENTITY_PATH = join(AGENTVALET_DIR, "agent.json");
|
|
23
|
+
export function getAgentvaletDir() {
|
|
24
|
+
return AGENTVALET_DIR;
|
|
25
|
+
}
|
|
26
|
+
export function getKeyPath() {
|
|
27
|
+
return KEY_PATH;
|
|
28
|
+
}
|
|
29
|
+
export function getIdentityPath() {
|
|
30
|
+
return IDENTITY_PATH;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads a previously-bound identity from disk, if one exists. Returns
|
|
34
|
+
* null when the file is absent or unparseable — callers should treat
|
|
35
|
+
* that as "no identity yet, proceed with bind".
|
|
36
|
+
*/
|
|
37
|
+
export function readBoundIdentity() {
|
|
38
|
+
if (!existsSync(IDENTITY_PATH))
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(IDENTITY_PATH, "utf-8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (typeof parsed.agent_id === "string" &&
|
|
44
|
+
typeof parsed.owner_id === "string" &&
|
|
45
|
+
typeof parsed.proxy_url === "string") {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns the disk-stored private key PEM if present, otherwise null.
|
|
56
|
+
*/
|
|
57
|
+
export function readBoundPrivateKey() {
|
|
58
|
+
if (!existsSync(KEY_PATH))
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
return readFileSync(KEY_PATH, "utf-8").trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Generates an RSA-2048 keypair, POSTs the public key + bind_secret to
|
|
69
|
+
* the proxy, and persists both the returned identity and the private
|
|
70
|
+
* key to ~/.agentvalet/.
|
|
71
|
+
*
|
|
72
|
+
* Caller is responsible for deciding when to invoke (e.g. only when
|
|
73
|
+
* INVITE_BIND_SECRET is set and no identity already exists on disk).
|
|
74
|
+
*/
|
|
75
|
+
export async function attemptInviteBind(opts) {
|
|
76
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
77
|
+
modulusLength: 2048,
|
|
78
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
79
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
80
|
+
});
|
|
81
|
+
const url = `${opts.proxyUrl.replace(/\/$/, "")}/v1/invites/bind`;
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
bind_secret: opts.bindSecret,
|
|
87
|
+
public_key_pem: publicKey,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
let detail = `HTTP ${res.status}`;
|
|
92
|
+
try {
|
|
93
|
+
const body = await res.json();
|
|
94
|
+
if (body.error)
|
|
95
|
+
detail = body.error;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
if (res.status === 410) {
|
|
101
|
+
throw new Error(`Bind secret rejected (${detail}). It was either already used or expired — ask the inviting manager to re-issue.`);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Invite bind failed: ${detail}`);
|
|
104
|
+
}
|
|
105
|
+
const payload = await res.json();
|
|
106
|
+
if (!payload.agent_id || !payload.owner_id || !payload.proxy_url) {
|
|
107
|
+
throw new Error("Bind succeeded but response was missing required fields");
|
|
108
|
+
}
|
|
109
|
+
const identity = {
|
|
110
|
+
agent_id: payload.agent_id,
|
|
111
|
+
owner_id: payload.owner_id,
|
|
112
|
+
proxy_url: payload.proxy_url,
|
|
113
|
+
bound_at: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
persistBindArtifacts(identity, privateKey);
|
|
116
|
+
return { identity, privateKeyPem: privateKey };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Writes the private key + identity to ~/.agentvalet/. The directory
|
|
120
|
+
* is created with 0700 if it does not yet exist; the key file is
|
|
121
|
+
* written with 0600. Identity is non-secret (no token material) but
|
|
122
|
+
* we keep it inside the same directory for cleanup symmetry.
|
|
123
|
+
*
|
|
124
|
+
* Exposed so tests can verify the on-disk layout without making a
|
|
125
|
+
* real HTTP call.
|
|
126
|
+
*/
|
|
127
|
+
export function persistBindArtifacts(identity, privateKeyPem) {
|
|
128
|
+
mkdirSync(AGENTVALET_DIR, { recursive: true, mode: 0o700 });
|
|
129
|
+
writeFileSync(KEY_PATH, privateKeyPem, { mode: 0o600 });
|
|
130
|
+
writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2));
|
|
131
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,25 +1,89 @@
|
|
|
1
1
|
import { readPrivateKeyFromEnv } from "./pem.js";
|
|
2
2
|
import { importPKCS8 } from "jose";
|
|
3
|
+
import { attemptInviteBind, readBoundIdentity, readBoundPrivateKey, } from "./bind.js";
|
|
4
|
+
const DEFAULT_PROXY_URL = "https://api.agentvalet.ai";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the agent's identity + private key. Three paths:
|
|
7
|
+
*
|
|
8
|
+
* 1. Env-based — AGENT_ID + OWNER_ID + PROXY_URL + AGENT_PRIVATE_KEY*
|
|
9
|
+
* provided. Legacy path; preserved unchanged.
|
|
10
|
+
*
|
|
11
|
+
* 2. Disk identity — env vars are missing but ~/.agentvalet/agent.json
|
|
12
|
+
* and ~/.agentvalet/agent.key exist (e.g. from a previous bind).
|
|
13
|
+
* Read them and proceed as if env had been set.
|
|
14
|
+
*
|
|
15
|
+
* 3. Invite-bind first run — INVITE_BIND_SECRET is set, no identity
|
|
16
|
+
* is yet on disk, and no AGENT_ID env. Generate a keypair, POST
|
|
17
|
+
* /v1/invites/bind, persist the result, then proceed.
|
|
18
|
+
*
|
|
19
|
+
* Order matters: env wins (explicit > implicit), then disk identity,
|
|
20
|
+
* then invite-bind. The bind path runs at most once per machine —
|
|
21
|
+
* the secret is consumed after first use.
|
|
22
|
+
*/
|
|
3
23
|
export async function validateConfig() {
|
|
24
|
+
// Path 1 — env-based (legacy).
|
|
25
|
+
const envAgentId = process.env.AGENT_ID;
|
|
26
|
+
const envOwnerId = process.env.OWNER_ID;
|
|
27
|
+
const envProxyUrl = process.env.PROXY_URL;
|
|
28
|
+
if (envAgentId && envOwnerId && envProxyUrl) {
|
|
29
|
+
return buildConfig({
|
|
30
|
+
agentId: envAgentId,
|
|
31
|
+
ownerId: envOwnerId,
|
|
32
|
+
proxyUrl: envProxyUrl,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Path 2 — disk identity from a previous bind.
|
|
36
|
+
const diskIdentity = readBoundIdentity();
|
|
37
|
+
const diskKey = readBoundPrivateKey();
|
|
38
|
+
if (diskIdentity && diskKey) {
|
|
39
|
+
return buildConfig({
|
|
40
|
+
agentId: envAgentId ?? diskIdentity.agent_id,
|
|
41
|
+
ownerId: envOwnerId ?? diskIdentity.owner_id,
|
|
42
|
+
proxyUrl: envProxyUrl ?? diskIdentity.proxy_url,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Path 3 — first-run invite bind.
|
|
46
|
+
const inviteBindSecret = process.env.INVITE_BIND_SECRET;
|
|
47
|
+
if (inviteBindSecret) {
|
|
48
|
+
const proxyUrl = (envProxyUrl ?? DEFAULT_PROXY_URL).replace(/\/$/, "");
|
|
49
|
+
process.stderr.write(`[mcp-server] First-run invite bind against ${proxyUrl}…\n`);
|
|
50
|
+
try {
|
|
51
|
+
const { identity } = await attemptInviteBind({
|
|
52
|
+
bindSecret: inviteBindSecret,
|
|
53
|
+
proxyUrl,
|
|
54
|
+
});
|
|
55
|
+
process.stderr.write(`[mcp-server] Bound as ${identity.agent_id} (owner ${identity.owner_id}). Key persisted to ~/.agentvalet/agent.key\n`);
|
|
56
|
+
return buildConfig({
|
|
57
|
+
agentId: identity.agent_id,
|
|
58
|
+
ownerId: identity.owner_id,
|
|
59
|
+
proxyUrl: identity.proxy_url,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
process.stderr.write(`[mcp-server] Invite bind failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Nothing usable — fall through to the original missing-env diagnostic.
|
|
4
68
|
const missing = [];
|
|
5
69
|
for (const key of ["AGENT_ID", "OWNER_ID", "PROXY_URL"]) {
|
|
6
70
|
if (!process.env[key])
|
|
7
71
|
missing.push(key);
|
|
8
72
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const proxyUrl =
|
|
73
|
+
process.stderr.write(`[mcp-server] Missing required environment variables: ${missing.join(", ")}\n` +
|
|
74
|
+
`Either set them, run the invite-bind flow with INVITE_BIND_SECRET, ` +
|
|
75
|
+
`or restore ~/.agentvalet/agent.{key,json} from a previous bind.\n`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
async function buildConfig(args) {
|
|
79
|
+
const proxyUrl = args.proxyUrl.replace(/\/$/, "");
|
|
16
80
|
let privateKeyPem = null;
|
|
17
81
|
let privateKey = null;
|
|
18
82
|
try {
|
|
19
83
|
privateKeyPem = readPrivateKeyFromEnv();
|
|
20
84
|
}
|
|
21
85
|
catch {
|
|
22
|
-
// No key
|
|
86
|
+
// No key — tools will return pending-activation response.
|
|
23
87
|
}
|
|
24
88
|
if (privateKeyPem !== null) {
|
|
25
89
|
try {
|
|
@@ -30,5 +94,5 @@ export async function validateConfig() {
|
|
|
30
94
|
process.exit(1);
|
|
31
95
|
}
|
|
32
96
|
}
|
|
33
|
-
return { agentId, ownerId, proxyUrl, privateKeyPem, privateKey };
|
|
97
|
+
return { agentId: args.agentId, ownerId: args.ownerId, proxyUrl, privateKeyPem, privateKey };
|
|
34
98
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
// ── BOOT DIAGNOSTICS — must be FIRST executable code ─────────────────────
|
|
2
|
+
// Claude Desktop captures whatever the MCP server writes to stderr before
|
|
3
|
+
// the stdio transport handshake. If startup crashes silently (top-level
|
|
4
|
+
// await rejection, import error, unhandled native crash), Claude Desktop
|
|
5
|
+
// only logs "Server transport closed unexpectedly" with no detail. These
|
|
6
|
+
// handlers guarantee a stack lands in the log no matter what fails next.
|
|
7
|
+
process.stderr.write(`[mcp-server] boot v0.2.x | node=${process.version} | platform=${process.platform} | ` +
|
|
8
|
+
`env_keys=${Object.keys(process.env).filter((k) => k.startsWith("AGENT_") || k === "OWNER_ID" || k === "PROXY_URL" || k === "INVITE_BIND_SECRET").join(",") || "(none of expected)"}\n`);
|
|
9
|
+
process.on("uncaughtException", (err) => {
|
|
10
|
+
process.stderr.write(`[mcp-server] FATAL uncaughtException: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
process.on("unhandledRejection", (err) => {
|
|
14
|
+
process.stderr.write(`[mcp-server] FATAL unhandledRejection: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
1
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
19
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -7,7 +23,16 @@ import { renderInstructions } from "./instructions.js";
|
|
|
7
23
|
// ---------------------------------------------------------------------------
|
|
8
24
|
// Startup env validation
|
|
9
25
|
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
26
|
+
let configResult;
|
|
27
|
+
try {
|
|
28
|
+
configResult = await validateConfig();
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
process.stderr.write(`[mcp-server] FATAL validateConfig threw: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const { agentId: AGENT_ID, ownerId: OWNER_ID, proxyUrl: PROXY_URL, privateKeyPem: AGENT_PRIVATE_KEY_RAW, privateKey } = configResult;
|
|
35
|
+
process.stderr.write(`[mcp-server] config ok | agent=${AGENT_ID} | owner=${OWNER_ID} | proxy=${PROXY_URL} | has_key=${!!privateKey}\n`);
|
|
11
36
|
// ---------------------------------------------------------------------------
|
|
12
37
|
// JWT signing
|
|
13
38
|
// ---------------------------------------------------------------------------
|
|
@@ -74,17 +99,17 @@ const LIST_PLATFORMS_TOOL = {
|
|
|
74
99
|
};
|
|
75
100
|
const USE_PLATFORM_TOOL = {
|
|
76
101
|
name: "use_platform",
|
|
77
|
-
description: "use_platform: Call an external platform API (Airtable, GitHub, Slack, etc.) through the AgentValet proxy.\nInput: platform (string), endpoint (string), method (GET|POST|PUT|PATCH|DELETE), scope (string),
|
|
102
|
+
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.",
|
|
78
103
|
inputSchema: {
|
|
79
104
|
type: "object",
|
|
80
105
|
properties: {
|
|
81
106
|
platform: {
|
|
82
107
|
type: "string",
|
|
83
|
-
description: "Platform ID (e.g. airtable, github, slack)",
|
|
108
|
+
description: "Platform ID (e.g. airtable, github, slack, metabase)",
|
|
84
109
|
},
|
|
85
110
|
endpoint: {
|
|
86
111
|
type: "string",
|
|
87
|
-
description: "API path on the target platform (e.g. /v0/meta/bases)",
|
|
112
|
+
description: "API path on the target platform (e.g. /v0/meta/bases or /api/dataset)",
|
|
88
113
|
},
|
|
89
114
|
method: {
|
|
90
115
|
type: "string",
|
|
@@ -95,9 +120,13 @@ const USE_PLATFORM_TOOL = {
|
|
|
95
120
|
type: "string",
|
|
96
121
|
description: "Permission scope required for this action (e.g. records:read)",
|
|
97
122
|
},
|
|
123
|
+
body: {
|
|
124
|
+
type: "object",
|
|
125
|
+
description: "JSON request body for POST/PUT/PATCH/DELETE. Optional. Forwarded verbatim to the upstream API.",
|
|
126
|
+
},
|
|
98
127
|
data: {
|
|
99
128
|
type: "object",
|
|
100
|
-
description: "
|
|
129
|
+
description: "Deprecated alias for `body` — prefer `body`. Kept for backwards compatibility.",
|
|
101
130
|
},
|
|
102
131
|
},
|
|
103
132
|
required: ["platform", "endpoint", "method", "scope"],
|
|
@@ -312,11 +341,20 @@ async function fetchPlatformNamesForInstructions() {
|
|
|
312
341
|
return undefined;
|
|
313
342
|
}
|
|
314
343
|
}
|
|
315
|
-
|
|
344
|
+
// NOTE: We intentionally do NOT prefetch platform names at boot. Doing so
|
|
345
|
+
// added a top-level await on the proxy and blocked the `initialize` response
|
|
346
|
+
// to Claude Desktop for several seconds (worse on cold Azure CA), causing
|
|
347
|
+
// host-side timeouts. The LLM learns the catalogue from list_platforms at
|
|
348
|
+
// runtime — boot-time enrichment was nice-to-have, not load-bearing.
|
|
316
349
|
// ---------------------------------------------------------------------------
|
|
317
350
|
// MCP server setup
|
|
318
351
|
// ---------------------------------------------------------------------------
|
|
319
|
-
const server = new Server({ name: "agentvalet", version: "1.0.0" }, { capabilities: { tools: {} }, instructions: renderInstructions(
|
|
352
|
+
const server = new Server({ name: "agentvalet", version: "1.0.0" }, { capabilities: { tools: {} }, instructions: renderInstructions(undefined) });
|
|
353
|
+
// Fire the platform-names fetch in the background after the server is up so
|
|
354
|
+
// it can't delay initialize. The result isn't surfaced to the host (MCP has
|
|
355
|
+
// no instructions-update message), but the network warmup primes the proxy
|
|
356
|
+
// and surfaces auth failures in the stderr boot diagnostics path.
|
|
357
|
+
void fetchPlatformNamesForInstructions().catch(() => { });
|
|
320
358
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
321
359
|
tools: [
|
|
322
360
|
LIST_PLATFORMS_TOOL,
|
|
@@ -351,12 +389,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
351
389
|
};
|
|
352
390
|
}
|
|
353
391
|
const progressToken = request.params._meta?.progressToken;
|
|
392
|
+
// Accept either `body` (canonical) or `data` (legacy alias). `body` wins
|
|
393
|
+
// when both are supplied. This matters because Claude (and other LLM
|
|
394
|
+
// hosts) reach for `body` as the natural HTTP terminology — the prior
|
|
395
|
+
// schema only declared `data` which made every POST get silently
|
|
396
|
+
// dropped to an empty body. See the use_platform tool description.
|
|
397
|
+
const bodyArg = (args.body ?? args.data);
|
|
354
398
|
return await handleUsePlatform({
|
|
355
399
|
platform: args.platform,
|
|
356
400
|
endpoint: args.endpoint,
|
|
357
401
|
method: args.method,
|
|
358
402
|
scope: args.scope,
|
|
359
|
-
data:
|
|
403
|
+
data: bodyArg,
|
|
360
404
|
}, progressToken);
|
|
361
405
|
}
|
|
362
406
|
if (name === "agent_register") {
|
|
@@ -469,6 +513,20 @@ async function handleListPlatforms() {
|
|
|
469
513
|
const body = await response.text();
|
|
470
514
|
if (!response.ok)
|
|
471
515
|
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
516
|
+
// Proxy wraps the payload as { data: { platforms, version }, _meta: {...} }
|
|
517
|
+
// but outputSchema declares the flat { platforms, version } shape, so the
|
|
518
|
+
// wrapped envelope fails strict structuredContent validation and the host
|
|
519
|
+
// surfaces "tool execution failed (no upstream body)". Unwrap .data.
|
|
520
|
+
const parsed = tryParseJson(body);
|
|
521
|
+
const inner = parsed && typeof parsed === "object" && parsed !== null && "data" in parsed
|
|
522
|
+
? parsed.data
|
|
523
|
+
: parsed;
|
|
524
|
+
if (inner && typeof inner === "object") {
|
|
525
|
+
return {
|
|
526
|
+
content: [{ type: "text", text: body }],
|
|
527
|
+
structuredContent: inner,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
472
530
|
return jsonContent(body);
|
|
473
531
|
}
|
|
474
532
|
// Translates a fetch() failure into something an end-user can actually act on.
|
package/dist/pem.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
+
import { readBoundPrivateKey } from "./bind.js";
|
|
2
3
|
/**
|
|
3
4
|
* Reads the RS256 private key from environment variables, supporting four formats:
|
|
4
5
|
* - AGENT_PRIVATE_KEY_B64: base64-encoded PEM
|
|
5
6
|
* - AGENT_PRIVATE_KEY_PATH: path to a PEM file
|
|
6
7
|
* - AGENT_PRIVATE_KEY: raw multi-line PEM or \n-escaped single-line PEM
|
|
8
|
+
*
|
|
9
|
+
* Falls back to the disk-persisted invite-bind key at
|
|
10
|
+
* ~/.agentvalet/agent.key when no env-based source is present. This is
|
|
11
|
+
* the path written by the invite-flow first-run bind (see bind.ts) —
|
|
12
|
+
* having pem.ts honour it means subsequent invocations of the
|
|
13
|
+
* mcp-server don't need any env vars at all.
|
|
7
14
|
*/
|
|
8
15
|
export function readPrivateKeyFromEnv() {
|
|
9
16
|
// 1. Base64-encoded PEM
|
|
@@ -30,5 +37,10 @@ export function readPrivateKeyFromEnv() {
|
|
|
30
37
|
}
|
|
31
38
|
return unescaped;
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
// 4. Disk fallback — written by the invite-bind first-run flow.
|
|
41
|
+
const diskKey = readBoundPrivateKey();
|
|
42
|
+
if (diskKey)
|
|
43
|
+
return diskKey;
|
|
44
|
+
throw new Error("No private key provided. Set AGENT_PRIVATE_KEY, AGENT_PRIVATE_KEY_PATH, or AGENT_PRIVATE_KEY_B64, " +
|
|
45
|
+
"or run the invite-bind flow with INVITE_BIND_SECRET.");
|
|
34
46
|
}
|