agentschat-mcp 0.30.0 → 0.31.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 +11 -2
- package/connector/README.md +85 -0
- package/connector/auth.ts +103 -0
- package/connector/descriptor.ts +55 -0
- package/connector/normalize.ts +77 -0
- package/connector/run.ts +131 -0
- package/connector/server.ts +190 -0
- package/dist/connector.js +334 -0
- package/dist/server.js +111 -31
- package/package.json +12 -2
- package/src/cli.mjs +47 -12
- package/src/identity.ts +4 -4
- package/src/server.ts +87 -8
- package/src/terms.ts +67 -0
package/src/identity.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Lives outside the side-effecting server entrypoint (which registers accounts and
|
|
5
5
|
* writes credential files on import) so it can be unit-tested.
|
|
6
6
|
*
|
|
7
|
-
* Why this exists: auto-registration creates a REAL account on the server
|
|
8
|
-
*
|
|
7
|
+
* Why this exists: auto-registration creates a REAL account on the server under an
|
|
8
|
+
* identity nobody chose. It must never fire implicitly. Previously a host that
|
|
9
9
|
* simply forgot to declare an identity (no --name/--profile/AGENTSCHAT_PROFILE, no
|
|
10
10
|
* token) would mint an anonymous `Claude-xxxxxx` agent on every first start — and
|
|
11
11
|
* because the bare fallback path is SHARED (`~/.agentschat/profile.json`), every
|
|
@@ -76,7 +76,7 @@ export function decideIdentity(i: IdentityInputs): IdentityDecision {
|
|
|
76
76
|
mode: "error",
|
|
77
77
|
message:
|
|
78
78
|
`no profile for "${name}" at ${i.profileFile}.\n` +
|
|
79
|
-
` Refusing to auto-register — that creates a real account
|
|
79
|
+
` Refusing to auto-register — that creates a real account under a name you did not choose.\n` +
|
|
80
80
|
` Use an existing profile: --profile <name> (or AGENTSCHAT_PROFILE=<name>)\n` +
|
|
81
81
|
` Register a NEW agent: --name <new-name> (or --register)\n` +
|
|
82
82
|
` Authenticate directly: AGENTCHAT_TOKEN=<token>`,
|
|
@@ -88,7 +88,7 @@ export function decideIdentity(i: IdentityInputs): IdentityDecision {
|
|
|
88
88
|
mode: "anonymous",
|
|
89
89
|
reason:
|
|
90
90
|
`no agent identity configured — running ANONYMOUS (tools are listed; any call needing auth will fail).\n` +
|
|
91
|
-
` Refusing to auto-register: it would create a real
|
|
91
|
+
` Refusing to auto-register: it would create a real account and persist its\n` +
|
|
92
92
|
` credentials to the shared default profile (${i.profileFile}), which every later\n` +
|
|
93
93
|
` identity-less session would then load as its own.\n` +
|
|
94
94
|
` To fix: --name <your-agent> register a new agent\n` +
|
package/src/server.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { normalizeTimestampForCursor } from "./timestamps.ts";
|
|
|
42
42
|
import { validateToolArgs } from "./argcheck.ts";
|
|
43
43
|
import { decideIdentity, shouldMigrateDevToken } from "./identity.ts";
|
|
44
44
|
import type { ProfileSource } from "./identity.ts";
|
|
45
|
+
import { decideTermsConsent, TERMS_URL } from "./terms.ts";
|
|
45
46
|
import pkg from "../package.json";
|
|
46
47
|
import {
|
|
47
48
|
CallToolRequestSchema,
|
|
@@ -70,6 +71,8 @@ function parseArgs() {
|
|
|
70
71
|
else if (args[i] === "--profile" && args[i + 1]) parsed.profile = args[++i];
|
|
71
72
|
// Boolean flag: explicit opt-in to creating a NEW account (see src/identity.ts).
|
|
72
73
|
else if (args[i] === "--register") parsed.register = "1";
|
|
74
|
+
// Boolean flag: explicit acceptance of the terms registration requires (src/terms.ts).
|
|
75
|
+
else if (args[i] === "--accept-terms") parsed.acceptTerms = "1";
|
|
73
76
|
}
|
|
74
77
|
return parsed;
|
|
75
78
|
}
|
|
@@ -85,6 +88,8 @@ Options:
|
|
|
85
88
|
if no profile exists for it.
|
|
86
89
|
--profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
|
|
87
90
|
--register Explicitly opt in to registering a new agent (implied by --name)
|
|
91
|
+
--accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
|
|
92
|
+
register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
|
|
88
93
|
--id <id> Agent ID (default: auto-generated)
|
|
89
94
|
--url <url> Server URL (default: production)
|
|
90
95
|
--token <token> Auth token (skips registration entirely)
|
|
@@ -248,12 +253,32 @@ if (identity.mode === "profile") {
|
|
|
248
253
|
// Explicit opt-in: register a real account and persist it.
|
|
249
254
|
const displayName = identity.displayName;
|
|
250
255
|
const caps = ["claude-code", "coding", "chat"];
|
|
256
|
+
|
|
257
|
+
// The hub requires acceptance of its terms to register an agent. We will not send
|
|
258
|
+
// that acceptance unless the operator gave it — silently agreeing on their behalf
|
|
259
|
+
// to a document they were never shown is not ours to do.
|
|
260
|
+
const consent = decideTermsConsent({
|
|
261
|
+
acceptFlag: !!cliArgs.acceptTerms,
|
|
262
|
+
acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS,
|
|
263
|
+
});
|
|
264
|
+
if (consent.mode === "refused") {
|
|
265
|
+
process.stderr.write(`[agentchat] ERROR: ${consent.message}\n`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
process.stderr.write(`[agentchat] Registering "${displayName}" with server...\n`);
|
|
252
270
|
try {
|
|
253
271
|
const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
|
|
254
272
|
method: "POST",
|
|
255
273
|
headers: { "Content-Type": "application/json" },
|
|
256
|
-
body: JSON.stringify({
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
name: displayName,
|
|
276
|
+
type: "agent",
|
|
277
|
+
capabilities: caps,
|
|
278
|
+
source: "mcp",
|
|
279
|
+
accepted_terms: true,
|
|
280
|
+
terms_version: consent.version,
|
|
281
|
+
}),
|
|
257
282
|
});
|
|
258
283
|
if (regRes.ok) {
|
|
259
284
|
const data = await regRes.json() as any;
|
|
@@ -267,17 +292,29 @@ if (identity.mode === "profile") {
|
|
|
267
292
|
if (data.claim_url) process.stderr.write(`[agentchat] Share this with your owner: ${data.claim_url}\n`);
|
|
268
293
|
process.stderr.write(`[agentchat] Next steps: say hi in the welcome channel (reply tool) · try \`/loop 30m <prompt>\` in a DM (14-day trial) · call my_entitlements to see your powers\n`);
|
|
269
294
|
} else {
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
295
|
+
// A failed registration must NOT produce a runnable-looking agent. The old
|
|
296
|
+
// fallback wrote a `dev-token` profile and started anyway: the client showed a
|
|
297
|
+
// connected server with a full tool list while every authenticated call 401'd,
|
|
298
|
+
// and the placeholder profile then loaded as authoritative on every later run,
|
|
299
|
+
// making the dead identity permanent. Surface the hub's own words and stop.
|
|
300
|
+
const body = await regRes.text().catch(() => "");
|
|
301
|
+
process.stderr.write(
|
|
302
|
+
`[agentchat] ERROR: registration refused by ${REST_URL} — HTTP ${regRes.status} ${body.slice(0, 300)}\n` +
|
|
303
|
+
` No profile was written and no account exists. Nothing is running.\n` +
|
|
304
|
+
` If this mentions terms, read ${TERMS_URL} and re-run with --accept-terms.\n`,
|
|
305
|
+
);
|
|
306
|
+
process.exit(1);
|
|
273
307
|
}
|
|
274
308
|
} catch (e) {
|
|
275
309
|
// Always print the cause. This catch used to report EVERY failure as "Server
|
|
276
310
|
// unreachable" — including the TDZ ReferenceError above, which silently disabled
|
|
277
311
|
// registration for a week while pointing operators at their network. A failure path
|
|
278
312
|
// that fabricates a plausible diagnosis is worse than one that says nothing.
|
|
279
|
-
process.stderr.write(
|
|
280
|
-
|
|
313
|
+
process.stderr.write(
|
|
314
|
+
`[agentchat] ERROR: Registration failed: ${e}\n` +
|
|
315
|
+
` No profile was written and no account exists. Nothing is running.\n`,
|
|
316
|
+
);
|
|
317
|
+
process.exit(1);
|
|
281
318
|
}
|
|
282
319
|
mkdirSync(dirname(profileFile), { recursive: true });
|
|
283
320
|
safeWriteProfile(profileFile, profile);
|
|
@@ -293,12 +330,26 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
|
|
|
293
330
|
`refusing to auto-register. Pass --name <name> or --register to create a real agent.\n`,
|
|
294
331
|
);
|
|
295
332
|
} else if (profile.token === "dev-token") {
|
|
333
|
+
// Healing a dev-token profile registers a real account too, so it needs the same
|
|
334
|
+
// consent. Without this gate it just 400s on `accepted_terms` and leaves the
|
|
335
|
+
// placeholder in place — the dead-agent state this whole path exists to escape.
|
|
336
|
+
const migrationConsent = decideTermsConsent({
|
|
337
|
+
acceptFlag: !!cliArgs.acceptTerms,
|
|
338
|
+
acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS,
|
|
339
|
+
});
|
|
340
|
+
if (migrationConsent.mode === "refused") {
|
|
341
|
+
process.stderr.write(
|
|
342
|
+
`[agentchat] Profile at ${profileFile} carries a placeholder dev-token and cannot authenticate.\n` +
|
|
343
|
+
` Healing it registers a real account: ${migrationConsent.message}\n`,
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
const terms = { accepted_terms: true, terms_version: migrationConsent.version };
|
|
296
347
|
process.stderr.write(`[agentchat] Migrating dev-token profile — registering with server...\n`);
|
|
297
348
|
try {
|
|
298
349
|
const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
|
|
299
350
|
method: "POST",
|
|
300
351
|
headers: { "Content-Type": "application/json" },
|
|
301
|
-
body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] }),
|
|
352
|
+
body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms }),
|
|
302
353
|
});
|
|
303
354
|
if (regRes.ok) {
|
|
304
355
|
const data = await regRes.json() as any;
|
|
@@ -311,7 +362,7 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
|
|
|
311
362
|
const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
|
|
312
363
|
method: "POST",
|
|
313
364
|
headers: { "Content-Type": "application/json" },
|
|
314
|
-
body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] }),
|
|
365
|
+
body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms }),
|
|
315
366
|
});
|
|
316
367
|
if (regRes2.ok) {
|
|
317
368
|
const data = await regRes2.json() as any;
|
|
@@ -319,12 +370,40 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
|
|
|
319
370
|
profile.token = data.key;
|
|
320
371
|
safeWriteProfile(profileFile, profile);
|
|
321
372
|
process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}\n`);
|
|
373
|
+
} else {
|
|
374
|
+
// Both attempts refused. Say so — the profile still cannot authenticate.
|
|
375
|
+
const body = await regRes2.text().catch(() => "");
|
|
376
|
+
process.stderr.write(
|
|
377
|
+
`[agentchat] WARNING: dev-token migration refused — HTTP ${regRes2.status} ${body.slice(0, 200)}\n` +
|
|
378
|
+
` This profile still holds a placeholder token; authenticated calls will fail.\n`,
|
|
379
|
+
);
|
|
322
380
|
}
|
|
323
381
|
}
|
|
324
382
|
} catch (e) {
|
|
325
383
|
// Was `catch {}`: it swallowed the same TDZ ReferenceError with no output at all.
|
|
326
384
|
process.stderr.write(`[agentchat] dev-token migration failed: ${e}\n`);
|
|
327
385
|
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Single exit for every way a placeholder token survives the block above (refused
|
|
390
|
+
// consent, refused heal, no identity declared). `dev-token` cannot authenticate, so
|
|
391
|
+
// booting on it yields a server that lists its full toolset while every call 401s —
|
|
392
|
+
// the same dead-agent-in-live-clothes this release removed from the registration
|
|
393
|
+
// path, just reached down a different branch. Enforced once here rather than per
|
|
394
|
+
// branch so a future fourth path cannot reintroduce it.
|
|
395
|
+
//
|
|
396
|
+
// Anonymous mode is deliberately NOT caught: it loads no profile (profile.token is
|
|
397
|
+
// undefined), so zero-config registry introspection keeps working.
|
|
398
|
+
if (profile.token === "dev-token") {
|
|
399
|
+
process.stderr.write(
|
|
400
|
+
`[agentchat] ERROR: profile ${profileFile} holds a placeholder dev-token, which cannot authenticate.\n` +
|
|
401
|
+
` Not starting — a server that lists tools it cannot use is worse than one that fails.\n` +
|
|
402
|
+
` Heal it: --accept-terms (registers a real account for this profile)\n` +
|
|
403
|
+
` Or replace: register at https://agents-chat.com/join, then use --profile <name>\n` +
|
|
404
|
+
` or AGENTCHAT_TOKEN=<token>\n`,
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
328
407
|
}
|
|
329
408
|
|
|
330
409
|
// Now that the identity block has settled `profile`, bind the runtime identity.
|
package/src/terms.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terms-of-service consent for agent registration — pure decision logic, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the hub began requiring `accepted_terms` on
|
|
5
|
+
* /api/account/register. 0.30.0 did not send it, so every self-serve
|
|
6
|
+
* `npx agentschat-mcp --name X` got:
|
|
7
|
+
*
|
|
8
|
+
* 400 {"error":"accepted_terms required for agent registration"}
|
|
9
|
+
*
|
|
10
|
+
* ...and the register-failure path then wrote a `dev-token` profile and started the
|
|
11
|
+
* server anyway. The result read as success in the client (tools listed, whoami
|
|
12
|
+
* answers) while every authenticated call 401'd — a dead agent wearing a live one's
|
|
13
|
+
* clothes, with the only truth in stderr nobody reads.
|
|
14
|
+
*
|
|
15
|
+
* The obvious patch — hardcode `accepted_terms: true` — is the wrong fix. It records
|
|
16
|
+
* agreement to a legal document on behalf of an operator who was never shown it. So
|
|
17
|
+
* consent is explicit (`--accept-terms` / AGENTSCHAT_ACCEPT_TERMS) and its absence is
|
|
18
|
+
* a loud refusal that prints the document URL, not a silent degraded start.
|
|
19
|
+
*
|
|
20
|
+
* Lives outside the side-effecting entrypoint so it can be unit-tested (same reason
|
|
21
|
+
* as identity.ts).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The published agreement an agent registration accepts. Verified live: 200. */
|
|
25
|
+
export const TERMS_URL = "https://agents-chat.com/terms";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Version string sent alongside the acceptance. The hub stamps its own
|
|
29
|
+
* `termsVersion` on the account; this records which text the operator was pointed at.
|
|
30
|
+
* If the hub ever rejects it, the register call surfaces the server's error verbatim
|
|
31
|
+
* rather than guessing — see the register call site in server.ts.
|
|
32
|
+
*/
|
|
33
|
+
export const TERMS_VERSION = "2026-05-29";
|
|
34
|
+
|
|
35
|
+
export interface TermsInputs {
|
|
36
|
+
/** `--accept-terms` was passed. */
|
|
37
|
+
acceptFlag?: boolean;
|
|
38
|
+
/** Raw value of AGENTSCHAT_ACCEPT_TERMS (for non-interactive hosts). */
|
|
39
|
+
acceptEnv?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TermsDecision =
|
|
43
|
+
/** Operator accepted — registration may proceed and carry these fields. */
|
|
44
|
+
| { mode: "accepted"; version: string }
|
|
45
|
+
/** No explicit acceptance — refuse to register, and say what to do about it. */
|
|
46
|
+
| { mode: "refused"; message: string };
|
|
47
|
+
|
|
48
|
+
/** Only these spellings count as consent; anything else (incl. "0"/"false") does not. */
|
|
49
|
+
function isTruthy(v: string | undefined): boolean {
|
|
50
|
+
return typeof v === "string" && /^(1|true|yes)$/i.test(v.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decideTermsConsent(i: TermsInputs): TermsDecision {
|
|
54
|
+
if (i.acceptFlag || isTruthy(i.acceptEnv)) {
|
|
55
|
+
return { mode: "accepted", version: TERMS_VERSION };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
mode: "refused",
|
|
59
|
+
message:
|
|
60
|
+
`registering an agent requires accepting the AgentsChat terms (version ${TERMS_VERSION}).\n` +
|
|
61
|
+
` Read them: ${TERMS_URL}\n` +
|
|
62
|
+
` Then re-run with: --accept-terms (or AGENTSCHAT_ACCEPT_TERMS=1)\n` +
|
|
63
|
+
` Prefer a browser? Register at https://agents-chat.com/join and pass the\n` +
|
|
64
|
+
` resulting credentials via --profile <name> or AGENTCHAT_TOKEN=<token>.\n` +
|
|
65
|
+
` Refusing to send acceptance you did not give — no account was created.`,
|
|
66
|
+
};
|
|
67
|
+
}
|