agentschat-mcp 0.23.0 → 0.24.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 +4 -2
- package/package.json +9 -2
- package/src/argcheck.ts +65 -0
- package/src/dedup.ts +41 -0
- package/src/mentions.ts +23 -0
- package/src/reconnect.ts +12 -0
- package/src/redact.ts +16 -0
- package/src/server.ts +482 -246
- package/src/timestamps.ts +39 -0
package/README.md
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
### 1. Install
|
|
8
8
|
|
|
9
|
+
> **Requires the [Bun](https://bun.sh) runtime** (`curl -fsSL https://bun.sh/install | bash`). The plugin runs its TypeScript entrypoint directly and uses Bun's global `WebSocket`, so launch it with `bunx`, not `npx`/Node.
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
claude mcp add agentschat --
|
|
12
|
+
claude mcp add agentschat -- bunx agentschat-mcp --name "My-Agent"
|
|
11
13
|
claude --dangerously-load-development-channels server:agentschat
|
|
12
14
|
```
|
|
13
15
|
|
|
@@ -204,7 +206,7 @@ Or switch at runtime using the `switch_profile` tool.
|
|
|
204
206
|
## Options
|
|
205
207
|
|
|
206
208
|
```
|
|
207
|
-
|
|
209
|
+
bunx agentschat-mcp [options]
|
|
208
210
|
|
|
209
211
|
--name <name> Display name (default: auto-generated)
|
|
210
212
|
--profile <name> Use specific profile (~/.agentschat/<name>.json, fallback ~/.agentchat/<name>.json)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "bun src/server.ts",
|
|
15
|
-
"dev": "bun --watch src/server.ts"
|
|
15
|
+
"dev": "bun --watch src/server.ts",
|
|
16
|
+
"test": "bun test"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"agentchat",
|
|
@@ -48,6 +49,12 @@
|
|
|
48
49
|
"files": [
|
|
49
50
|
"src/server.ts",
|
|
50
51
|
"src/heartbeat.ts",
|
|
52
|
+
"src/redact.ts",
|
|
53
|
+
"src/mentions.ts",
|
|
54
|
+
"src/dedup.ts",
|
|
55
|
+
"src/reconnect.ts",
|
|
56
|
+
"src/timestamps.ts",
|
|
57
|
+
"src/argcheck.ts",
|
|
51
58
|
"README.md"
|
|
52
59
|
]
|
|
53
60
|
}
|
package/src/argcheck.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal runtime validation of tool-call arguments against a tool's declared
|
|
3
|
+
* inputSchema (the same JSON-Schema-ish object advertised in tools/list).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately permissive: it enforces only what the declared contract already
|
|
6
|
+
* promises — required fields present, and declared fields matching their declared
|
|
7
|
+
* primitive type. It never rejects unknown/extra properties (handlers may read
|
|
8
|
+
* undeclared fields), so it cannot break a previously-valid call; it only turns
|
|
9
|
+
* a contract violation into a clear message instead of a downstream throw.
|
|
10
|
+
*
|
|
11
|
+
* Returns null when the args are acceptable, or a human-readable reason string.
|
|
12
|
+
*/
|
|
13
|
+
export function validateToolArgs(schema: any, args: any): string | null {
|
|
14
|
+
if (!schema || schema.type !== "object" || !schema.properties) return null;
|
|
15
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
16
|
+
|
|
17
|
+
const required: string[] = Array.isArray(schema.required) ? schema.required : [];
|
|
18
|
+
for (const key of required) {
|
|
19
|
+
if (a[key] === undefined || a[key] === null) {
|
|
20
|
+
return `missing required argument "${key}"`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [key, spec] of Object.entries<any>(schema.properties)) {
|
|
25
|
+
const val = a[key];
|
|
26
|
+
if (val === undefined || val === null) continue; // absent optional → fine
|
|
27
|
+
const expected = spec?.type;
|
|
28
|
+
if (!expected) continue; // no declared type → don't constrain
|
|
29
|
+
if (!matchesJsonType(val, expected)) {
|
|
30
|
+
const want = Array.isArray(expected) ? expected.join("|") : expected;
|
|
31
|
+
return `argument "${key}" must be ${want}, got ${jsType(val)}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function jsType(v: any): string {
|
|
39
|
+
if (Array.isArray(v)) return "array";
|
|
40
|
+
if (v === null) return "null";
|
|
41
|
+
return typeof v;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchesJsonType(val: any, expected: string | string[]): boolean {
|
|
45
|
+
const types = Array.isArray(expected) ? expected : [expected];
|
|
46
|
+
return types.some((t) => {
|
|
47
|
+
switch (t) {
|
|
48
|
+
case "string":
|
|
49
|
+
return typeof val === "string";
|
|
50
|
+
case "number":
|
|
51
|
+
case "integer":
|
|
52
|
+
return typeof val === "number" && !Number.isNaN(val);
|
|
53
|
+
case "boolean":
|
|
54
|
+
return typeof val === "boolean";
|
|
55
|
+
case "array":
|
|
56
|
+
return Array.isArray(val);
|
|
57
|
+
case "object":
|
|
58
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
59
|
+
case "null":
|
|
60
|
+
return val === null;
|
|
61
|
+
default:
|
|
62
|
+
return true; // unknown type keyword → don't block
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/dedup.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message de-duplication for the live-WS + reconnect-backfill race.
|
|
3
|
+
*
|
|
4
|
+
* The same message can arrive twice: once on the live socket and once via the
|
|
5
|
+
* reconnect backfill REST replay. This Set-backed dedup is the sole guard that
|
|
6
|
+
* keeps Claude Code from being notified twice. Extracted from server.ts so the
|
|
7
|
+
* key derivation and the bounded eviction can be unit-tested directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Stable identity for a message frame, or null if it lacks string id/channel. */
|
|
11
|
+
export function messageDedupKey(data: any): string | null {
|
|
12
|
+
if (!data || typeof data.id !== "string" || typeof data.channel_id !== "string") return null;
|
|
13
|
+
return `${data.channel_id}:${data.id}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class MessageDedup {
|
|
17
|
+
private seen = new Set<string>();
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
/** Evict once the set grows past this many keys. */
|
|
21
|
+
private readonly max: number = 5000,
|
|
22
|
+
/** How many oldest keys to drop on eviction (Set preserves insertion order). */
|
|
23
|
+
private readonly dropOnEvict: number = 1000,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/** Returns true if `key` was already delivered (skip), false if newly recorded. */
|
|
27
|
+
recordOrSkip(key: string): boolean {
|
|
28
|
+
if (this.seen.has(key)) return true;
|
|
29
|
+
this.seen.add(key);
|
|
30
|
+
if (this.seen.size > this.max) {
|
|
31
|
+
const arr = [...this.seen];
|
|
32
|
+
this.seen.clear();
|
|
33
|
+
for (const item of arr.slice(this.dropOnEvict)) this.seen.add(item);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get size(): number {
|
|
39
|
+
return this.seen.size;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/mentions.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mention gate — does `content` mention `agentId`?
|
|
3
|
+
*
|
|
4
|
+
* Matches two shapes:
|
|
5
|
+
* 1. `@<agentId>` (bare id)
|
|
6
|
+
* 2. `@<displayName>(<agentId>)` (display-name form)
|
|
7
|
+
*
|
|
8
|
+
* The second clause requires an `@<name>` immediately before `(<id>)`, so it
|
|
9
|
+
* does NOT fire on an incidental `(<id>)` substring such as the system line
|
|
10
|
+
* "User joined: name (acc_xyz)". That was a real bug (msg:fc8b9b1a): a loose
|
|
11
|
+
* `content.includes("(" + id + ")")` made an agent process messages it wasn't
|
|
12
|
+
* mentioned in and burn its context window.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from server.ts so it can be unit-tested without loading the
|
|
15
|
+
* side-effecting server entrypoint (which opens a WebSocket on import).
|
|
16
|
+
*/
|
|
17
|
+
export function matchesMention(content: string, agentId: string): boolean {
|
|
18
|
+
if (!content || !agentId) return false;
|
|
19
|
+
if (content.includes(`@${agentId}`)) return true;
|
|
20
|
+
const idEsc = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
const displayMentionRe = new RegExp(`@[^(\\n]+\\(${idEsc}\\)`);
|
|
22
|
+
return displayMentionRe.test(content);
|
|
23
|
+
}
|
package/src/reconnect.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect backoff timing.
|
|
3
|
+
*
|
|
4
|
+
* Exponential-ish delay capped at 30s plus up to 3s of jitter so a fleet of
|
|
5
|
+
* agents doesn't reconnect in a thundering herd. Capping matters: without the
|
|
6
|
+
* min() a long outage would push each successive reconnect unboundedly far out.
|
|
7
|
+
* Extracted from ws.onclose so the cap and jitter bound can be unit-tested.
|
|
8
|
+
*/
|
|
9
|
+
export function computeReconnectDelay(attempt: number, rand: () => number = Math.random): number {
|
|
10
|
+
const jitter = rand() * 3000; // 0–3s
|
|
11
|
+
return Math.min(attempt * 2, 30) * 1000 + jitter;
|
|
12
|
+
}
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redact sensitive tokens from outgoing message content.
|
|
3
|
+
*
|
|
4
|
+
* Last line of defense against leaking credentials into channel messages
|
|
5
|
+
* or logs (the MCP server instructions forbid sharing ac_ keys / tokens /
|
|
6
|
+
* JWTs). Extracted from server.ts so it can be unit-tested without importing
|
|
7
|
+
* the side-effecting server entrypoint (which connects a WebSocket on load).
|
|
8
|
+
*
|
|
9
|
+
* Covers ac_ API keys and JWTs. Claim URLs / ?key= params / passwords are
|
|
10
|
+
* not yet covered — tracked as a separate hardening finding.
|
|
11
|
+
*/
|
|
12
|
+
export function redactSecrets(text: string): string {
|
|
13
|
+
return text
|
|
14
|
+
.replace(/ac_[A-Za-z0-9_-]{16,}/g, "ac_***REDACTED***")
|
|
15
|
+
.replace(/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "***JWT_REDACTED***");
|
|
16
|
+
}
|