@sleep2agi/commhub-server 0.5.0-preview.37 → 0.5.0-preview.38
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/package.json +1 -1
- package/src/auth.ts +8 -2
- package/src/index.ts +11 -6
- package/src/tools.ts +21 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.5.0-preview.
|
|
3
|
+
"version": "0.5.0-preview.38",
|
|
4
4
|
"description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 18 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/auth.ts
CHANGED
|
@@ -126,10 +126,11 @@ export function createNetworkTokenForNode(userId: string, networkId: string, nod
|
|
|
126
126
|
return { ok: true, token };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
export function resolveToken(token: string): { user: AuthUser; networkId: string | null } | null {
|
|
129
|
+
export function resolveToken(token: string): { user: AuthUser; networkId: string | null; tokenName: string | null } | null {
|
|
130
130
|
const tHash = hashToken(token);
|
|
131
131
|
const row = db.get<any>(
|
|
132
|
-
`SELECT t.user_id, t.network_id, t.scope,
|
|
132
|
+
`SELECT t.user_id, t.network_id, t.scope, t.name AS token_name,
|
|
133
|
+
u.username, u.display_name, u.email, u.role
|
|
133
134
|
FROM api_tokens t JOIN users u ON t.user_id = u.user_id
|
|
134
135
|
WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > datetime('now'))`,
|
|
135
136
|
tHash);
|
|
@@ -142,6 +143,11 @@ export function resolveToken(token: string): { user: AuthUser; networkId: string
|
|
|
142
143
|
return {
|
|
143
144
|
user: { user_id: row.user_id, username: row.username, display_name: row.display_name, email: row.email, role: row.role },
|
|
144
145
|
networkId: row.network_id,
|
|
146
|
+
// tokenName carries the binding identity. For node-scoped ntok_, it's
|
|
147
|
+
// 'node:<alias>'; we strip the prefix and use it as the default
|
|
148
|
+
// from_session for any MCP send_task / send_message / etc, so peer
|
|
149
|
+
// agents see who actually called them (not 'hub').
|
|
150
|
+
tokenName: row.token_name || null,
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
|
package/src/index.ts
CHANGED
|
@@ -42,12 +42,12 @@ setInterval(() => {
|
|
|
42
42
|
}, 300000);
|
|
43
43
|
|
|
44
44
|
// ── Factory: 每个请求创建新的 McpServer(stateless 模式)──
|
|
45
|
-
function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null): McpServer {
|
|
45
|
+
function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null): McpServer {
|
|
46
46
|
const server = new McpServer({
|
|
47
47
|
name: "commhub",
|
|
48
48
|
version: "0.5.0",
|
|
49
49
|
});
|
|
50
|
-
registerTools(server, clientIP, enforceNetworkId, enforceUserId);
|
|
50
|
+
registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias);
|
|
51
51
|
return server;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -70,15 +70,15 @@ function requireAuth(req: Request): Response | null {
|
|
|
70
70
|
return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Extract user + network from request token
|
|
74
|
-
function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string } | null {
|
|
73
|
+
// Extract user + network + token-binding identity from request token.
|
|
74
|
+
function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string; tokenName: string | null } | null {
|
|
75
75
|
const header = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
76
76
|
const url = new URL(req.url);
|
|
77
77
|
const token = header || url.searchParams.get("token") || "";
|
|
78
78
|
if (!token) return null;
|
|
79
79
|
const resolved = resolveToken(token);
|
|
80
80
|
if (!resolved) return null;
|
|
81
|
-
return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username };
|
|
81
|
+
return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username, tokenName: resolved.tokenName };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
type RestNetworkScope = {
|
|
@@ -242,10 +242,15 @@ Bun.serve({
|
|
|
242
242
|
// (which logs in as a user) cannot call send_task.
|
|
243
243
|
const authCtx = resolveRequestAuth(req);
|
|
244
244
|
const enforceNetId = authCtx?.networkId || null;
|
|
245
|
+
// Derive the calling alias from the token name (e.g., 'node:视频审查')
|
|
246
|
+
// so peer agents see the real sender instead of 'hub' on send_task.
|
|
247
|
+
const callerAlias = authCtx?.tokenName?.startsWith("node:")
|
|
248
|
+
? authCtx.tokenName.slice("node:".length)
|
|
249
|
+
: (authCtx?.username || null);
|
|
245
250
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
246
251
|
sessionIdGenerator: undefined,
|
|
247
252
|
});
|
|
248
|
-
const server = createServer(clientIP, enforceNetId, authCtx?.userId || null);
|
|
253
|
+
const server = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias);
|
|
249
254
|
await server.connect(transport);
|
|
250
255
|
const response = await transport.handleRequest(req);
|
|
251
256
|
// Disconnect after response to prevent McpServer leak
|
package/src/tools.ts
CHANGED
|
@@ -8,7 +8,13 @@ function ts(): string {
|
|
|
8
8
|
return new Date().toTimeString().slice(0, 8);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null) {
|
|
11
|
+
export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null) {
|
|
12
|
+
// Default from_session for outbound tools — extracted from the calling
|
|
13
|
+
// token's binding (ntok_ → node alias, utok_ → username). Without this,
|
|
14
|
+
// an agent's send_task call always claimed from='hub' and peer agents
|
|
15
|
+
// couldn't tell who actually asked them. Tool callers can still override
|
|
16
|
+
// by passing from_session explicitly.
|
|
17
|
+
const defaultFrom = (clientFrom?: string) => clientFrom || callerAlias || "hub";
|
|
12
18
|
// If enforceNetworkId is set, override any client-supplied network_id
|
|
13
19
|
const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
|
|
14
20
|
|
|
@@ -385,11 +391,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
385
391
|
task: z.string().min(1).max(10000).describe("Task content"),
|
|
386
392
|
priority: z.enum(["high", "normal", "low"]).optional().default("normal"),
|
|
387
393
|
context: z.string().max(10000).optional(),
|
|
388
|
-
from_session: z.string().max(200).optional()
|
|
394
|
+
from_session: z.string().max(200).optional(),
|
|
389
395
|
ttl_seconds: z.number().min(1).max(86400).optional().describe("Task TTL in seconds (default: 3600)"),
|
|
390
396
|
network_id: z.string().max(200).optional().describe("Network scope"),
|
|
391
397
|
},
|
|
392
|
-
async ({ alias, task, priority, context, from_session, ttl_seconds, network_id: netId }) => {
|
|
398
|
+
async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId }) => { const from_session = defaultFrom(_fromIn);
|
|
393
399
|
const effectiveNetId = getNetworkId(netId);
|
|
394
400
|
|
|
395
401
|
// Role check: viewer cannot send tasks
|
|
@@ -462,9 +468,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
462
468
|
{
|
|
463
469
|
alias: z.string().min(1).max(200).describe("Target session alias"),
|
|
464
470
|
message: z.string().min(1).max(10000).describe("Message content"),
|
|
465
|
-
from_session: z.string().max(200).optional()
|
|
471
|
+
from_session: z.string().max(200).optional(),
|
|
466
472
|
},
|
|
467
|
-
async ({ alias, message, from_session }) => {
|
|
473
|
+
async ({ alias, message, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
468
474
|
const effectiveNetId = getNetworkId(null);
|
|
469
475
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
470
476
|
console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
|
|
@@ -503,9 +509,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
503
509
|
text: z.string().min(1).max(10000).describe("Reply content"),
|
|
504
510
|
in_reply_to: z.string().max(200).optional().describe("Original task/message ID"),
|
|
505
511
|
status: z.enum(["replied", "failed", "cancelled"]).optional().default("replied").describe("Task outcome"),
|
|
506
|
-
from_session: z.string().max(200).optional()
|
|
512
|
+
from_session: z.string().max(200).optional(),
|
|
507
513
|
},
|
|
508
|
-
async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
|
|
514
|
+
async ({ alias, text, in_reply_to, status: replyStatus, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
509
515
|
const effectiveNetId = getNetworkId(null);
|
|
510
516
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
511
517
|
console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
|
|
@@ -554,9 +560,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
554
560
|
"Acknowledge receipt of a task. Does NOT enter inbox. Updates task status only.",
|
|
555
561
|
{
|
|
556
562
|
task_id: z.string().min(1).max(200).describe("Task ID to acknowledge"),
|
|
557
|
-
from_session: z.string().max(200).optional()
|
|
563
|
+
from_session: z.string().max(200).optional(),
|
|
558
564
|
},
|
|
559
|
-
async ({ task_id, from_session }) => {
|
|
565
|
+
async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
560
566
|
const effectiveNetId = getNetworkId(null);
|
|
561
567
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
562
568
|
console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
|
|
@@ -580,9 +586,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
580
586
|
"Retry a failed, expired, or cancelled task. Resets status to delivered and re-queues in inbox.",
|
|
581
587
|
{
|
|
582
588
|
task_id: z.string().min(1).max(200).describe("Task ID to retry"),
|
|
583
|
-
from_session: z.string().max(200).optional()
|
|
589
|
+
from_session: z.string().max(200).optional(),
|
|
584
590
|
},
|
|
585
|
-
async ({ task_id, from_session }) => {
|
|
591
|
+
async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
586
592
|
const effectiveNetId = getNetworkId(null);
|
|
587
593
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
588
594
|
console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
|
|
@@ -689,9 +695,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
689
695
|
{
|
|
690
696
|
task_id: z.string().min(1).max(200).describe("Task ID to cancel"),
|
|
691
697
|
reason: z.string().max(1000).optional().describe("Cancellation reason"),
|
|
692
|
-
from_session: z.string().max(200).optional()
|
|
698
|
+
from_session: z.string().max(200).optional(),
|
|
693
699
|
},
|
|
694
|
-
async ({ task_id, reason, from_session }) => {
|
|
700
|
+
async ({ task_id, reason, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
695
701
|
const effectiveNetId = getNetworkId(null);
|
|
696
702
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
697
703
|
console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
|
|
@@ -721,9 +727,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
721
727
|
{
|
|
722
728
|
task_id: z.string().min(1).max(200).describe("Task ID to reassign"),
|
|
723
729
|
new_alias: z.string().min(1).max(200).describe("Target agent alias"),
|
|
724
|
-
from_session: z.string().max(200).optional()
|
|
730
|
+
from_session: z.string().max(200).optional(),
|
|
725
731
|
},
|
|
726
|
-
async ({ task_id, new_alias, from_session }) => {
|
|
732
|
+
async ({ task_id, new_alias, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
727
733
|
const effectiveNetId = getNetworkId(null);
|
|
728
734
|
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
729
735
|
console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
|