@agentvalet/mcp-server 0.3.9 → 1.1.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/bind.js +26 -8
- package/dist/config.js +43 -11
- package/dist/index.js +49 -16
- package/dist/pem.js +22 -6
- package/package.json +1 -1
package/dist/bind.js
CHANGED
|
@@ -79,14 +79,32 @@ export async function attemptInviteBind(opts) {
|
|
|
79
79
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
80
80
|
});
|
|
81
81
|
const url = `${opts.proxyUrl.replace(/\/$/, "")}/v1/invites/bind`;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// 15s hard timeout: Claude Desktop kills the transport at ~12-15s of
|
|
83
|
+
// silence. If the proxy hangs, fail visibly with stderr rather than
|
|
84
|
+
// letting the host record an unexplained "transport closed".
|
|
85
|
+
const ac = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => ac.abort(), 15_000);
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
bind_secret: opts.bindSecret,
|
|
94
|
+
public_key_pem: publicKey,
|
|
95
|
+
}),
|
|
96
|
+
signal: ac.signal,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
101
|
+
throw new Error("Bind request timed out after 15s — proxy unreachable or slow.");
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
}
|
|
90
108
|
if (!res.ok) {
|
|
91
109
|
let detail = `HTTP ${res.status}`;
|
|
92
110
|
try {
|
package/dist/config.js
CHANGED
|
@@ -20,11 +20,27 @@ const DEFAULT_PROXY_URL = "https://api.agentvalet.ai";
|
|
|
20
20
|
* then invite-bind. The bind path runs at most once per machine —
|
|
21
21
|
* the secret is consumed after first use.
|
|
22
22
|
*/
|
|
23
|
+
// Treat empty strings AND unresolved MCPB template placeholders (e.g.
|
|
24
|
+
// "${user_config.agent_id}") as "not set". Claude Desktop on some platforms
|
|
25
|
+
// passes the literal placeholder through when a user_config field has no
|
|
26
|
+
// value and no manifest default — that would otherwise false-trigger the
|
|
27
|
+
// env-based Path 1 below and skip the invite-bind handshake.
|
|
28
|
+
function envOrNull(name) {
|
|
29
|
+
const v = process.env[name];
|
|
30
|
+
if (v === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
const t = v.trim();
|
|
33
|
+
if (t === "")
|
|
34
|
+
return null;
|
|
35
|
+
if (t.startsWith("${") && t.endsWith("}"))
|
|
36
|
+
return null;
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
23
39
|
export async function validateConfig() {
|
|
24
40
|
// Path 1 — env-based (legacy).
|
|
25
|
-
const envAgentId =
|
|
26
|
-
const envOwnerId =
|
|
27
|
-
const envProxyUrl =
|
|
41
|
+
const envAgentId = envOrNull("AGENT_ID");
|
|
42
|
+
const envOwnerId = envOrNull("OWNER_ID");
|
|
43
|
+
const envProxyUrl = envOrNull("PROXY_URL");
|
|
28
44
|
if (envAgentId && envOwnerId && envProxyUrl) {
|
|
29
45
|
return buildConfig({
|
|
30
46
|
agentId: envAgentId,
|
|
@@ -43,7 +59,7 @@ export async function validateConfig() {
|
|
|
43
59
|
});
|
|
44
60
|
}
|
|
45
61
|
// Path 3 — first-run invite bind.
|
|
46
|
-
const inviteBindSecret =
|
|
62
|
+
const inviteBindSecret = envOrNull("INVITE_BIND_SECRET");
|
|
47
63
|
if (inviteBindSecret) {
|
|
48
64
|
const proxyUrl = (envProxyUrl ?? DEFAULT_PROXY_URL).replace(/\/$/, "");
|
|
49
65
|
process.stderr.write(`[mcp-server] First-run invite bind against ${proxyUrl}…\n`);
|
|
@@ -64,16 +80,28 @@ export async function validateConfig() {
|
|
|
64
80
|
process.exit(1);
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
|
-
// Nothing usable —
|
|
83
|
+
// Nothing usable — boot credential-less instead of exiting.
|
|
84
|
+
//
|
|
85
|
+
// Introspection (initialize + tools/list) must work with NO credentials so
|
|
86
|
+
// a sandbox (e.g. Glama) can start the server and read the static tool
|
|
87
|
+
// catalogue. We therefore return a config with an EMPTY identity and a NULL
|
|
88
|
+
// key rather than process.exit(1). The credential requirement is deferred to
|
|
89
|
+
// tools/call: an authed tool invoked with no usable identity returns a clean
|
|
90
|
+
// "credentials not configured" tool result (see index.ts requireCredentials),
|
|
91
|
+
// never a crash. An empty agentId is the signal for "state C — not configured"
|
|
92
|
+
// (distinct from "state B — identity present, key pending" which keeps the
|
|
93
|
+
// existing invite-bind pending response).
|
|
68
94
|
const missing = [];
|
|
69
95
|
for (const key of ["AGENT_ID", "OWNER_ID", "PROXY_URL"]) {
|
|
70
96
|
if (!process.env[key])
|
|
71
97
|
missing.push(key);
|
|
72
98
|
}
|
|
73
|
-
process.stderr.write(`[mcp-server]
|
|
74
|
-
`
|
|
75
|
-
`
|
|
76
|
-
|
|
99
|
+
process.stderr.write(`[mcp-server] No credentials configured (${missing.join(", ")} unset, no disk ` +
|
|
100
|
+
`identity, no INVITE_BIND_SECRET). Booting in introspection-only mode: ` +
|
|
101
|
+
`tools/list works; tools/call returns a credentials-required message. ` +
|
|
102
|
+
`Set the env vars, run the invite-bind flow, or restore ` +
|
|
103
|
+
`~/.agentvalet/agent.{key,json} to enable platform calls.\n`);
|
|
104
|
+
return buildConfig({ agentId: "", ownerId: "", proxyUrl: DEFAULT_PROXY_URL });
|
|
77
105
|
}
|
|
78
106
|
async function buildConfig(args) {
|
|
79
107
|
const proxyUrl = args.proxyUrl.replace(/\/$/, "");
|
|
@@ -90,8 +118,12 @@ async function buildConfig(args) {
|
|
|
90
118
|
privateKey = await importPKCS8(privateKeyPem, "RS256");
|
|
91
119
|
}
|
|
92
120
|
catch (err) {
|
|
93
|
-
process
|
|
94
|
-
|
|
121
|
+
// Don't crash the process on a malformed key — that would also take down
|
|
122
|
+
// introspection. Drop the unusable key to null and log; tools/call will
|
|
123
|
+
// return the credentials-required message instead of a hard exit.
|
|
124
|
+
process.stderr.write(`[mcp-server] Invalid private key (ignored): ${err instanceof Error ? err.message : err}\n`);
|
|
125
|
+
privateKeyPem = null;
|
|
126
|
+
privateKey = null;
|
|
95
127
|
}
|
|
96
128
|
}
|
|
97
129
|
return { agentId: args.agentId, ownerId: args.ownerId, proxyUrl, privateKeyPem, privateKey };
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,40 @@ function pendingFirstCallResponse() {
|
|
|
68
68
|
isError: true,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
// State C — no credentials at all (empty env / Glama-style sandbox). The
|
|
72
|
+
// server still boots and answers introspection; an authed tool call lands
|
|
73
|
+
// here and gets an actionable message instead of a crash. Distinct from the
|
|
74
|
+
// state-B "owner confirmation pending" response above, which means an identity
|
|
75
|
+
// IS configured but its key hasn't arrived yet.
|
|
76
|
+
function credentialsNotConfiguredResponse() {
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: "AgentValet credentials are not configured. Set AGENTVALET_AGENT_ID, " +
|
|
81
|
+
"AGENTVALET_OWNER_ID, and the agent private key (and optionally " +
|
|
82
|
+
"AGENTVALET_PROXY_URL). Run npx @agentvalet/register to create an agent. " +
|
|
83
|
+
"Docs: https://github.com/AgentValet/AgentValet#quickstart",
|
|
84
|
+
}],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Credential gate for authed tools/call. Returns null when the call may
|
|
89
|
+
// proceed (state A — a JWT can be signed), or a ready-to-return MCP tool
|
|
90
|
+
// result for the two no-key states:
|
|
91
|
+
// • state B (identity present, key pending) → existing invite-bind pending
|
|
92
|
+
// response, preserving the MCPB first-run flow.
|
|
93
|
+
// • state C (no identity at all) → credentials-not-configured.
|
|
94
|
+
// The no-auth tools (agent_register / agent_status) intentionally never call
|
|
95
|
+
// this — you must be able to register in order to OBTAIN credentials.
|
|
96
|
+
async function requireCredentials() {
|
|
97
|
+
if (AGENT_PRIVATE_KEY_RAW !== null)
|
|
98
|
+
return null;
|
|
99
|
+
if (AGENT_ID && OWNER_ID) {
|
|
100
|
+
await notifyBindSecret();
|
|
101
|
+
return pendingFirstCallResponse();
|
|
102
|
+
}
|
|
103
|
+
return credentialsNotConfiguredResponse();
|
|
104
|
+
}
|
|
71
105
|
// ---------------------------------------------------------------------------
|
|
72
106
|
// Tool definitions
|
|
73
107
|
// ---------------------------------------------------------------------------
|
|
@@ -499,10 +533,9 @@ function tryParseJson(text) {
|
|
|
499
533
|
// Tool handlers
|
|
500
534
|
// ---------------------------------------------------------------------------
|
|
501
535
|
async function handleListPlatforms() {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
return
|
|
505
|
-
}
|
|
536
|
+
const gate = await requireCredentials();
|
|
537
|
+
if (gate)
|
|
538
|
+
return gate;
|
|
506
539
|
let response;
|
|
507
540
|
try {
|
|
508
541
|
response = await fetchWithAuth(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
|
|
@@ -558,10 +591,9 @@ const APPROVAL_POLL_BUDGET_MS = 50_000;
|
|
|
558
591
|
const APPROVAL_POLL_INTERVAL_MS = 2_000;
|
|
559
592
|
const APPROVAL_PROGRESS_INTERVAL_MS = 5_000;
|
|
560
593
|
async function handleUsePlatform(params, progressToken) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return
|
|
564
|
-
}
|
|
594
|
+
const gate = await requireCredentials();
|
|
595
|
+
if (gate)
|
|
596
|
+
return gate;
|
|
565
597
|
const requestBody = {
|
|
566
598
|
platform: params.platform,
|
|
567
599
|
endpoint: params.endpoint,
|
|
@@ -729,6 +761,9 @@ async function handleAgentStatus(token) {
|
|
|
729
761
|
return jsonContent(body);
|
|
730
762
|
}
|
|
731
763
|
async function handleAuthzenEvaluate(platformId, scope) {
|
|
764
|
+
const gate = await requireCredentials();
|
|
765
|
+
if (gate)
|
|
766
|
+
return gate;
|
|
732
767
|
const authzenBody = {
|
|
733
768
|
subject: { type: "agent", id: AGENT_ID },
|
|
734
769
|
action: { name: "tool_call" },
|
|
@@ -750,10 +785,9 @@ async function handleAuthzenEvaluate(platformId, scope) {
|
|
|
750
785
|
return jsonContent(body);
|
|
751
786
|
}
|
|
752
787
|
async function handleListMyPendingActions() {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return
|
|
756
|
-
}
|
|
788
|
+
const gate = await requireCredentials();
|
|
789
|
+
if (gate)
|
|
790
|
+
return gate;
|
|
757
791
|
let response;
|
|
758
792
|
try {
|
|
759
793
|
response = await fetchWithAuth(`${PROXY_URL}/v1/agents/me/pending-actions`, { method: "GET" });
|
|
@@ -767,10 +801,9 @@ async function handleListMyPendingActions() {
|
|
|
767
801
|
return jsonContent(text);
|
|
768
802
|
}
|
|
769
803
|
async function handleReportSelfDiagnostic(args) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return
|
|
773
|
-
}
|
|
804
|
+
const gate = await requireCredentials();
|
|
805
|
+
if (gate)
|
|
806
|
+
return gate;
|
|
774
807
|
// Whitelist body fields — never forward owner_id/agent_id (proxy derives those from JWT).
|
|
775
808
|
const body = {
|
|
776
809
|
severity: args.severity,
|
package/dist/pem.js
CHANGED
|
@@ -12,23 +12,39 @@ import { readBoundPrivateKey } from "./bind.js";
|
|
|
12
12
|
* having pem.ts honour it means subsequent invocations of the
|
|
13
13
|
* mcp-server don't need any env vars at all.
|
|
14
14
|
*/
|
|
15
|
+
// Treat empty + unresolved MCPB placeholders ("${user_config.foo}") as
|
|
16
|
+
// not-set — see config.ts for the same defence on identity envs.
|
|
17
|
+
function readEnv(name) {
|
|
18
|
+
const v = process.env[name];
|
|
19
|
+
if (v === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
const t = v.trim();
|
|
22
|
+
if (t === "")
|
|
23
|
+
return null;
|
|
24
|
+
if (t.startsWith("${") && t.endsWith("}"))
|
|
25
|
+
return null;
|
|
26
|
+
return t;
|
|
27
|
+
}
|
|
15
28
|
export function readPrivateKeyFromEnv() {
|
|
16
29
|
// 1. Base64-encoded PEM
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
const b64 = readEnv("AGENT_PRIVATE_KEY_B64");
|
|
31
|
+
if (b64) {
|
|
32
|
+
return Buffer.from(b64, "base64").toString("utf-8").trim();
|
|
19
33
|
}
|
|
20
34
|
// 2. Path to PEM file
|
|
21
|
-
|
|
35
|
+
const path = readEnv("AGENT_PRIVATE_KEY_PATH");
|
|
36
|
+
if (path) {
|
|
22
37
|
try {
|
|
23
|
-
return readFileSync(
|
|
38
|
+
return readFileSync(path, "utf-8").trim();
|
|
24
39
|
}
|
|
25
40
|
catch (err) {
|
|
26
41
|
throw new Error(`Cannot read AGENT_PRIVATE_KEY_PATH: ${err instanceof Error ? err.message : err}`);
|
|
27
42
|
}
|
|
28
43
|
}
|
|
29
44
|
// 3. Raw PEM content (multi-line or \n-escaped)
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
const rawEnv = readEnv("AGENT_PRIVATE_KEY");
|
|
46
|
+
if (rawEnv) {
|
|
47
|
+
const raw = rawEnv;
|
|
32
48
|
// Unescape \n sequences
|
|
33
49
|
const unescaped = raw.replace(/\\n/g, "\n");
|
|
34
50
|
// Wrap bare base64 blob without headers
|