agorai 0.4.4 → 0.6.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 +29 -9
- package/dist/bridge/server.d.ts +1 -1
- package/dist/bridge/server.d.ts.map +1 -1
- package/dist/bridge/server.js +402 -8
- package/dist/bridge/server.js.map +1 -1
- package/dist/bridge/tools.d.ts +230 -7
- package/dist/bridge/tools.d.ts.map +1 -1
- package/dist/bridge/tools.js +92 -2
- package/dist/bridge/tools.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/store/events.d.ts +20 -1
- package/dist/store/events.d.ts.map +1 -1
- package/dist/store/events.js +24 -0
- package/dist/store/events.js.map +1 -1
- package/dist/store/interfaces.d.ts +31 -1
- package/dist/store/interfaces.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts +36 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +497 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/types.d.ts +109 -0
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,12 +75,16 @@ Your PC / VPS
|
|
|
75
75
|
│ │
|
|
76
76
|
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
|
|
77
77
|
│ │ Projects │ │ Convos │ │ Shared Memory │ │
|
|
78
|
-
│ │
|
|
78
|
+
│ │ + Tasks │ │ + Whisper │ │ + Agent Memory │ │
|
|
79
79
|
│ └──────────┘ └───────────┘ └──────────────────┘ │
|
|
80
80
|
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
|
|
81
81
|
│ │ Auth │ │ Rate │ │ 4-level │ │
|
|
82
82
|
│ │ (salted) │ │ limiting │ │ visibility │ │
|
|
83
83
|
│ └──────────┘ └───────────┘ └──────────────────┘ │
|
|
84
|
+
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
|
|
85
|
+
│ │ Capabil. │ │ Skills │ │ 35 MCP tools │ │
|
|
86
|
+
│ │ catalog │ │ system │ │ + SSE push │ │
|
|
87
|
+
│ └──────────┘ └───────────┘ └──────────────────┘ │
|
|
84
88
|
│ SQLite │
|
|
85
89
|
└────────────────────┬─────────────────────────────┘
|
|
86
90
|
│ HTTP (MCP protocol)
|
|
@@ -94,7 +98,7 @@ Your PC / VPS
|
|
|
94
98
|
|
|
95
99
|
Two npm packages:
|
|
96
100
|
|
|
97
|
-
- **`agorai`** — The bridge server. Hosts projects, conversations, shared memory, auth, and
|
|
101
|
+
- **`agorai`** — The bridge server. Hosts projects, conversations, shared memory, auth, and 35 MCP tools over HTTP. SQLite storage, zero external services. Can also run internal agents in the same process via `--with-agent`.
|
|
98
102
|
- **`agorai-connect`** — Connects any agent to the bridge. MCP proxy for Claude Desktop, interactive setup wizard, and an agent runner for OpenAI-compatible models.
|
|
99
103
|
|
|
100
104
|
## Key features
|
|
@@ -120,6 +124,18 @@ Two npm packages:
|
|
|
120
124
|
npx agorai debate "Redis vs Memcached for session storage?"
|
|
121
125
|
```
|
|
122
126
|
|
|
127
|
+
**Task claiming** — Create tasks with required capabilities, claim them atomically (no race conditions), complete with results. Stale claims auto-release when agents go offline. Pull model — agents discover and claim work, not push.
|
|
128
|
+
|
|
129
|
+
**Directed messages (whisper)** — Send private messages to specific agents with `recipients`. Only listed agents and the sender can see the message. Store-enforced — non-recipients never know the message exists. Additive to visibility: both filters apply.
|
|
130
|
+
|
|
131
|
+
**Capability discovery** — Agents register capabilities on connect. `discover_capabilities` lets agents find each other by skill (`code-review`, `analysis`, `code-execution`). The foundation for intelligent task routing.
|
|
132
|
+
|
|
133
|
+
**Skills system** — Progressive disclosure skills replace instructions. Skills have title, summary, instructions hint, full content, and supporting files. Agents receive only metadata (tier 1) on subscribe — load full content (tier 2) and files (tier 3) on demand. Target skills to specific agents by name or by type/capability selector. Tags for filtering. ~80-90% context savings.
|
|
134
|
+
|
|
135
|
+
**Agent memory** — Private per-agent scratchpad with 3 scopes: global, per-project, and per-conversation. Each agent manages its own memory — invisible to other agents. Conversation memory auto-cleans on unsubscribe.
|
|
136
|
+
|
|
137
|
+
**Message tags** — Tag messages with metadata (`review`, `urgent`, `decision`) and filter by tags or sender in `get_messages`. Structured message types (`proposal`, `decision`) enable formal conversation protocols.
|
|
138
|
+
|
|
123
139
|
**Structured metadata** — Every message carries trusted `bridgeMetadata` (visibility, capping info, confidentiality instructions) and private `agentMetadata` (only visible to the sender). Agents can't forge bridge data.
|
|
124
140
|
|
|
125
141
|
**Security** — Salted HMAC-SHA-256 API key hashing, per-agent rate limiting, input size limits on all fields, visibility-capped writes. Everything localhost by default.
|
|
@@ -140,15 +156,16 @@ docker run -v ./agorai.config.json:/app/agorai.config.json -p 3100:3100 agorai/b
|
|
|
140
156
|
|
|
141
157
|
| Version | Focus |
|
|
142
158
|
|---------|-------|
|
|
143
|
-
| **v0.2** | **Bridge — shared workspace, visibility, auth,
|
|
159
|
+
| **v0.2** | **Bridge — shared workspace, visibility, auth, MCP tools** |
|
|
144
160
|
| v0.2.x | Security hardening, Docker, npm publish, session recovery, internal agents |
|
|
145
161
|
| **v0.3** | **SSE push notifications — real-time message delivery, 3-layer EventBus→Dispatcher→Client** |
|
|
146
|
-
| **v0.4** | **Metadata overhaul — bridgeMetadata/agentMetadata, confidentiality modes,
|
|
147
|
-
| v0.
|
|
148
|
-
| v0.
|
|
149
|
-
| v0.
|
|
150
|
-
| v0.
|
|
151
|
-
| v0.
|
|
162
|
+
| **v0.4** | **Metadata overhaul — bridgeMetadata/agentMetadata, confidentiality modes, access requests** |
|
|
163
|
+
| **v0.5** | **Discover, Decide, Deliver — 32 tools: capability catalog, task claiming, whispers, message tags, agent memory, instruction matrix, structured protocol** |
|
|
164
|
+
| **v0.6** | **Skills system — progressive disclosure (3-tier), agent targeting, skill files, replaces instruction matrix. 35 tools** |
|
|
165
|
+
| v0.7 | Task dependencies, explicit project access control, full-text search, conversation templates |
|
|
166
|
+
| v0.8 | Orchestrator agent, Sentinel AI, debate engine via bridge |
|
|
167
|
+
| v0.9 | Web dashboard, human participants, A2A protocol support |
|
|
168
|
+
| v1.0+ | Enterprise — OAuth/JWT, RBAC, audit trail, SaaS |
|
|
152
169
|
|
|
153
170
|
## Positioning
|
|
154
171
|
|
|
@@ -160,6 +177,9 @@ Agorai is **not** another agent framework. It's infrastructure — the collabora
|
|
|
160
177
|
| Protocol | MCP (open standard) | Custom | Custom | Custom |
|
|
161
178
|
| Models | Any (BYOM) | OpenAI-focused | OpenAI-focused | LangChain |
|
|
162
179
|
| Visibility | 4-level, store-enforced | None | None | None |
|
|
180
|
+
| Task claiming | Atomic, capability-based | Role assignment | None | DAG nodes |
|
|
181
|
+
| Agent memory | Private per-agent, 3 scopes | Shared only | Shared only | None |
|
|
182
|
+
| Directed messages | Whisper (recipients) | None | None | None |
|
|
163
183
|
| Debate/consensus | Built-in | None | Basic | None |
|
|
164
184
|
| Local-first | Yes | Cloud-centric | Cloud-centric | Cloud-centric |
|
|
165
185
|
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge HTTP server — Streamable HTTP transport for the MCP bridge.
|
|
3
3
|
*
|
|
4
|
-
* Exposes
|
|
4
|
+
* Exposes 35 bridge tools + debate tools over HTTP.
|
|
5
5
|
* Auth is handled via API key in Authorization header.
|
|
6
6
|
* Each request is authenticated before being passed to the MCP handler.
|
|
7
7
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/bridge/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/bridge/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA2G3C,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AA+6BD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC,CAqLD"}
|
package/dist/bridge/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge HTTP server — Streamable HTTP transport for the MCP bridge.
|
|
3
3
|
*
|
|
4
|
-
* Exposes
|
|
4
|
+
* Exposes 35 bridge tools + debate tools over HTTP.
|
|
5
5
|
* Auth is handled via API key in Authorization header.
|
|
6
6
|
* Each request is authenticated before being passed to the MCP handler.
|
|
7
7
|
*/
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { resolve, dirname } from "node:path";
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const PKG_VERSION = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8")).version;
|
|
19
|
-
import { RegisterAgentSchema, ListBridgeAgentsSchema, CreateProjectSchema, ListProjectsSchema, SetMemorySchema, GetMemorySchema, DeleteMemorySchema, CreateConversationSchema, ListConversationsSchema, SubscribeSchema, UnsubscribeSchema, SendMessageSchema, GetMessagesSchema, GetStatusSchema, MarkReadSchema, ListSubscribersSchema, ListAccessRequestsSchema, RespondToAccessRequestSchema, GetMyAccessRequestsSchema, } from "./tools.js";
|
|
19
|
+
import { RegisterAgentSchema, ListBridgeAgentsSchema, DiscoverCapabilitiesSchema, CreateProjectSchema, ListProjectsSchema, SetMemorySchema, GetMemorySchema, DeleteMemorySchema, CreateConversationSchema, ListConversationsSchema, SubscribeSchema, UnsubscribeSchema, SendMessageSchema, GetMessagesSchema, GetStatusSchema, MarkReadSchema, ListSubscribersSchema, ListAccessRequestsSchema, RespondToAccessRequestSchema, GetMyAccessRequestsSchema, CreateTaskSchema, ListTasksSchema, ClaimTaskSchema, CompleteTaskSchema, ReleaseTaskSchema, UpdateTaskSchema, SetSkillSchema, ListSkillsSchema, GetSkillSchema, DeleteSkillSchema, SetSkillFileSchema, GetSkillFileSchema, SetAgentMemorySchema, GetAgentMemorySchema, DeleteAgentMemorySchema, } from "./tools.js";
|
|
20
20
|
const log = createLogger("bridge");
|
|
21
21
|
/** Per-session context: maps transport sessionId → agent auth. */
|
|
22
22
|
const sessionAuth = new Map();
|
|
@@ -98,6 +98,13 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
98
98
|
"If you try to subscribe to a conversation you don't have access to, an access request is created automatically.",
|
|
99
99
|
"Subscribers of that conversation can approve or deny your request via list_access_requests + respond_to_access_request.",
|
|
100
100
|
"Check your request status with get_my_access_requests.",
|
|
101
|
+
"",
|
|
102
|
+
"IMPORTANT — Skills system (progressive disclosure):",
|
|
103
|
+
"Skills provide behavioral instructions and context. They use 3-tier progressive disclosure to save context:",
|
|
104
|
+
"- Tier 1 (metadata): When you subscribe, you receive skill metadata (title, summary, instructions, tags) — NOT the full content.",
|
|
105
|
+
"- Tier 2 (content): Call get_skill(skill_id) to load the full content of a skill you need.",
|
|
106
|
+
"- Tier 3 (files): Call get_skill_file(skill_id, filename) to load supporting files attached to a skill.",
|
|
107
|
+
"Only load tier 2/3 when you actually need the detail. The summary and instructions fields give you enough to decide.",
|
|
101
108
|
].join("\n"),
|
|
102
109
|
});
|
|
103
110
|
// --- Agent tools ---
|
|
@@ -137,6 +144,17 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
137
144
|
const safe = filtered.map(({ apiKeyHash: _, ...rest }) => rest);
|
|
138
145
|
return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
|
|
139
146
|
});
|
|
147
|
+
server.tool("discover_capabilities", "Find agents by capability. Without a filter, returns all agents and their capabilities.", DiscoverCapabilitiesSchema.shape, async (args) => {
|
|
148
|
+
let agents;
|
|
149
|
+
if (args.capability) {
|
|
150
|
+
agents = await store.findAgentsByCapability(args.capability);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
agents = await store.listAgents();
|
|
154
|
+
}
|
|
155
|
+
const safe = agents.map(({ apiKeyHash: _, ...rest }) => rest);
|
|
156
|
+
return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
|
|
157
|
+
});
|
|
140
158
|
// --- Project tools ---
|
|
141
159
|
server.tool("create_project", "Create a new project", CreateProjectSchema.shape, async (args) => {
|
|
142
160
|
const project = await store.createProject({
|
|
@@ -203,9 +221,20 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
203
221
|
defaultVisibility: args.default_visibility,
|
|
204
222
|
createdBy: agentId,
|
|
205
223
|
});
|
|
206
|
-
// Auto-subscribe the creator
|
|
224
|
+
// Auto-subscribe the creator and include skills metadata
|
|
207
225
|
await store.subscribe(conv.id, agentId);
|
|
208
|
-
|
|
226
|
+
const agent = await store.getAgent(agentId);
|
|
227
|
+
const matchingSkills = agent
|
|
228
|
+
? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, conv.id)
|
|
229
|
+
: [];
|
|
230
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
231
|
+
...conv,
|
|
232
|
+
skills: matchingSkills.map((s) => ({
|
|
233
|
+
id: s.id, title: s.title, summary: s.summary,
|
|
234
|
+
instructions: s.instructions, tags: s.tags,
|
|
235
|
+
scope: s.scope, agents: s.agents, files: s.files,
|
|
236
|
+
})),
|
|
237
|
+
}, null, 2) }] };
|
|
209
238
|
});
|
|
210
239
|
server.tool("list_conversations", "List conversations in a project", ListConversationsSchema.shape, async (args) => {
|
|
211
240
|
let conversations = await store.listConversations(args.project_id, agentId);
|
|
@@ -215,10 +244,23 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
215
244
|
return { content: [{ type: "text", text: JSON.stringify(conversations, null, 2) }] };
|
|
216
245
|
});
|
|
217
246
|
server.tool("subscribe", "Subscribe to a conversation. If you don't have access, an access request is created automatically — existing subscribers can approve it.", SubscribeSchema.shape, async (args) => {
|
|
218
|
-
//
|
|
247
|
+
// If already subscribed, return current skills metadata (not an error)
|
|
219
248
|
const alreadySubscribed = await store.isSubscribed(args.conversation_id, agentId);
|
|
220
249
|
if (alreadySubscribed) {
|
|
221
|
-
|
|
250
|
+
const agent = await store.getAgent(agentId);
|
|
251
|
+
const matchingSkills = agent
|
|
252
|
+
? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, args.conversation_id)
|
|
253
|
+
: [];
|
|
254
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
255
|
+
subscribed: true,
|
|
256
|
+
already_subscribed: true,
|
|
257
|
+
conversation_id: args.conversation_id,
|
|
258
|
+
skills: matchingSkills.map((s) => ({
|
|
259
|
+
id: s.id, title: s.title, summary: s.summary,
|
|
260
|
+
instructions: s.instructions, tags: s.tags,
|
|
261
|
+
scope: s.scope, agents: s.agents, files: s.files,
|
|
262
|
+
})),
|
|
263
|
+
}) }] };
|
|
222
264
|
}
|
|
223
265
|
// Verify conversation exists and caller can access its project
|
|
224
266
|
const conv = await store.getConversation(args.conversation_id);
|
|
@@ -230,7 +272,25 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
230
272
|
await store.subscribe(args.conversation_id, agentId, {
|
|
231
273
|
historyAccess: args.history_access,
|
|
232
274
|
});
|
|
233
|
-
|
|
275
|
+
// Include matching skill metadata in subscribe response (progressive disclosure: tier 1 only)
|
|
276
|
+
const agent = await store.getAgent(agentId);
|
|
277
|
+
const matchingSkills = agent
|
|
278
|
+
? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, args.conversation_id)
|
|
279
|
+
: [];
|
|
280
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
281
|
+
subscribed: true,
|
|
282
|
+
conversation_id: args.conversation_id,
|
|
283
|
+
skills: matchingSkills.map((s) => ({
|
|
284
|
+
id: s.id,
|
|
285
|
+
title: s.title,
|
|
286
|
+
summary: s.summary,
|
|
287
|
+
instructions: s.instructions,
|
|
288
|
+
tags: s.tags,
|
|
289
|
+
scope: s.scope,
|
|
290
|
+
agents: s.agents,
|
|
291
|
+
files: s.files,
|
|
292
|
+
})),
|
|
293
|
+
}) }] };
|
|
234
294
|
}
|
|
235
295
|
// No project access — fallback to access request
|
|
236
296
|
// NOTE: Currently triggered by clearance < project visibility. In v0.6, access control
|
|
@@ -280,12 +340,30 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
280
340
|
const subscribed = await store.isSubscribed(args.conversation_id, agentId);
|
|
281
341
|
if (!subscribed)
|
|
282
342
|
return ACCESS_DENIED;
|
|
343
|
+
// Validate @mentions in whispers: mentioned agents must be in recipients list
|
|
344
|
+
if (args.recipients && args.recipients.length > 0) {
|
|
345
|
+
const allAgents = await store.listAgents();
|
|
346
|
+
const agentNameMap = new Map(allAgents.map((a) => [a.name.toLowerCase(), a.id]));
|
|
347
|
+
const mentionPattern = /@([\w-]+)/g;
|
|
348
|
+
let match;
|
|
349
|
+
while ((match = mentionPattern.exec(args.content)) !== null) {
|
|
350
|
+
const mentionedName = match[1].toLowerCase();
|
|
351
|
+
const mentionedId = agentNameMap.get(mentionedName);
|
|
352
|
+
if (mentionedId && mentionedId !== agentId && !args.recipients.includes(mentionedId)) {
|
|
353
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
354
|
+
error: `@${match[1]} is mentioned but not in recipients — they won't see this whisper. Add them to recipients or remove the @mention.`,
|
|
355
|
+
}) }] };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
283
359
|
const message = await store.sendMessage({
|
|
284
360
|
conversationId: args.conversation_id,
|
|
285
361
|
fromAgent: agentId,
|
|
286
362
|
type: args.type,
|
|
287
363
|
visibility: args.visibility,
|
|
288
364
|
content: args.content,
|
|
365
|
+
tags: args.tags,
|
|
366
|
+
recipients: args.recipients,
|
|
289
367
|
metadata: args.metadata,
|
|
290
368
|
});
|
|
291
369
|
// Response: include bridgeMetadata + agentMetadata (sender sees their own), exclude deprecated metadata
|
|
@@ -301,6 +379,8 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
301
379
|
since: args.since,
|
|
302
380
|
unreadOnly: args.unread_only,
|
|
303
381
|
limit: args.limit,
|
|
382
|
+
tags: args.tags,
|
|
383
|
+
fromAgent: args.from_agent,
|
|
304
384
|
});
|
|
305
385
|
// Shape response: strip agentMetadata for non-sender, exclude deprecated metadata
|
|
306
386
|
const shaped = messages.map((msg) => {
|
|
@@ -312,6 +392,272 @@ function createBridgeMcpServer(store, agentId) {
|
|
|
312
392
|
});
|
|
313
393
|
return { content: [{ type: "text", text: JSON.stringify(shaped, null, 2) }] };
|
|
314
394
|
});
|
|
395
|
+
// --- Task tools ---
|
|
396
|
+
server.tool("create_task", "Create a task in a project. Other agents can discover and claim it.", CreateTaskSchema.shape, async (args) => {
|
|
397
|
+
const project = await store.getProject(args.project_id, agentId);
|
|
398
|
+
if (!project)
|
|
399
|
+
return ACCESS_DENIED;
|
|
400
|
+
const task = await store.createTask({
|
|
401
|
+
projectId: args.project_id,
|
|
402
|
+
conversationId: args.conversation_id,
|
|
403
|
+
title: args.title,
|
|
404
|
+
description: args.description,
|
|
405
|
+
requiredCapabilities: args.required_capabilities,
|
|
406
|
+
createdBy: agentId,
|
|
407
|
+
});
|
|
408
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
409
|
+
});
|
|
410
|
+
server.tool("list_tasks", "List tasks in a project, optionally filtered by status, capability, or claiming agent", ListTasksSchema.shape, async (args) => {
|
|
411
|
+
const tasks = await store.listTasks(args.project_id, agentId, {
|
|
412
|
+
status: args.status,
|
|
413
|
+
claimedBy: args.claimed_by,
|
|
414
|
+
capability: args.capability,
|
|
415
|
+
});
|
|
416
|
+
return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
|
|
417
|
+
});
|
|
418
|
+
server.tool("claim_task", "Claim an open task. Atomic — only one agent can claim a task at a time.", ClaimTaskSchema.shape, async (args) => {
|
|
419
|
+
const task = await store.claimTask(args.task_id, agentId);
|
|
420
|
+
if (!task) {
|
|
421
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Task not available — it may already be claimed or does not exist" }) }] };
|
|
422
|
+
}
|
|
423
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
424
|
+
});
|
|
425
|
+
server.tool("complete_task", "Mark a claimed task as completed with an optional result", CompleteTaskSchema.shape, async (args) => {
|
|
426
|
+
const task = await store.completeTask(args.task_id, agentId, args.result);
|
|
427
|
+
if (!task) {
|
|
428
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot complete — task is not claimed by you or does not exist" }) }] };
|
|
429
|
+
}
|
|
430
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
431
|
+
});
|
|
432
|
+
server.tool("release_task", "Release a claimed task back to open so another agent can claim it", ReleaseTaskSchema.shape, async (args) => {
|
|
433
|
+
const task = await store.releaseTask(args.task_id, agentId);
|
|
434
|
+
if (!task) {
|
|
435
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot release — task is not claimed or you lack permission" }) }] };
|
|
436
|
+
}
|
|
437
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
438
|
+
});
|
|
439
|
+
server.tool("update_task", "Update a task you created (title, description, or status). Only the creator can update.", UpdateTaskSchema.shape, async (args) => {
|
|
440
|
+
const task = await store.updateTask(args.task_id, agentId, {
|
|
441
|
+
title: args.title,
|
|
442
|
+
description: args.description,
|
|
443
|
+
status: args.status,
|
|
444
|
+
});
|
|
445
|
+
if (!task) {
|
|
446
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot update — task not found or you are not the creator" }) }] };
|
|
447
|
+
}
|
|
448
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
449
|
+
});
|
|
450
|
+
// --- Skill tools ---
|
|
451
|
+
server.tool("set_skill", "Create or update a skill in a scope. Creator can always create/edit. Listed agents (in agents[]) can edit existing skills. Use selector and agents[] to target specific agents.", SetSkillSchema.shape, async (args) => {
|
|
452
|
+
let scope = "bridge";
|
|
453
|
+
let scopeId;
|
|
454
|
+
if (args.conversation_id) {
|
|
455
|
+
scope = "conversation";
|
|
456
|
+
scopeId = args.conversation_id;
|
|
457
|
+
const conv = await store.getConversation(args.conversation_id);
|
|
458
|
+
if (!conv)
|
|
459
|
+
return ACCESS_DENIED;
|
|
460
|
+
// Creator can always set. Listed agents can edit existing.
|
|
461
|
+
if (conv.createdBy !== agentId) {
|
|
462
|
+
// Check if caller is a listed agent on an existing skill with this title
|
|
463
|
+
const existing = (await store.listSkills("conversation", args.conversation_id)).find((s) => s.title === args.title);
|
|
464
|
+
if (!existing) {
|
|
465
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the conversation creator can create new skills" }) }] };
|
|
466
|
+
}
|
|
467
|
+
const agent = await store.getAgent(agentId);
|
|
468
|
+
const agentName = agent?.name ?? "";
|
|
469
|
+
if (!existing.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
|
|
470
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can edit this skill" }) }] };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (args.project_id) {
|
|
475
|
+
scope = "project";
|
|
476
|
+
scopeId = args.project_id;
|
|
477
|
+
const project = await store.getProject(args.project_id, agentId);
|
|
478
|
+
if (!project)
|
|
479
|
+
return ACCESS_DENIED;
|
|
480
|
+
if (project.createdBy !== agentId) {
|
|
481
|
+
const existing = (await store.listSkills("project", args.project_id)).find((s) => s.title === args.title);
|
|
482
|
+
if (!existing) {
|
|
483
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the project creator can create new skills" }) }] };
|
|
484
|
+
}
|
|
485
|
+
const agent = await store.getAgent(agentId);
|
|
486
|
+
const agentName = agent?.name ?? "";
|
|
487
|
+
if (!existing.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
|
|
488
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can edit this skill" }) }] };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Bridge-level skills cannot be set via MCP. Provide project_id or conversation_id." }) }] };
|
|
494
|
+
}
|
|
495
|
+
const skill = await store.setSkill({
|
|
496
|
+
scope,
|
|
497
|
+
scopeId,
|
|
498
|
+
title: args.title,
|
|
499
|
+
summary: args.summary,
|
|
500
|
+
instructions: args.instructions,
|
|
501
|
+
selector: args.selector,
|
|
502
|
+
agents: args.agents,
|
|
503
|
+
tags: args.tags,
|
|
504
|
+
content: args.content,
|
|
505
|
+
createdBy: agentId,
|
|
506
|
+
});
|
|
507
|
+
return { content: [{ type: "text", text: JSON.stringify(skill, null, 2) }] };
|
|
508
|
+
});
|
|
509
|
+
server.tool("list_skills", "List skills for a scope (returns metadata only — no content). No params = bridge-level. With project_id = project-level. With conversation_id = conversation-level.", ListSkillsSchema.shape, async (args) => {
|
|
510
|
+
let scope = "bridge";
|
|
511
|
+
let scopeId;
|
|
512
|
+
if (args.conversation_id) {
|
|
513
|
+
scope = "conversation";
|
|
514
|
+
scopeId = args.conversation_id;
|
|
515
|
+
const subscribed = await store.isSubscribed(args.conversation_id, agentId);
|
|
516
|
+
if (!subscribed)
|
|
517
|
+
return ACCESS_DENIED;
|
|
518
|
+
}
|
|
519
|
+
else if (args.project_id) {
|
|
520
|
+
scope = "project";
|
|
521
|
+
scopeId = args.project_id;
|
|
522
|
+
const project = await store.getProject(args.project_id, agentId);
|
|
523
|
+
if (!project)
|
|
524
|
+
return ACCESS_DENIED;
|
|
525
|
+
}
|
|
526
|
+
const skills = await store.listSkills(scope, scopeId, { tags: args.tags });
|
|
527
|
+
// Filter by agent visibility
|
|
528
|
+
const agent = await store.getAgent(agentId);
|
|
529
|
+
const agentName = agent?.name ?? "";
|
|
530
|
+
const visible = skills.filter((s) => {
|
|
531
|
+
if (s.agents.length === 0)
|
|
532
|
+
return true;
|
|
533
|
+
return s.agents.some((a) => a.toLowerCase() === agentName.toLowerCase());
|
|
534
|
+
});
|
|
535
|
+
// Return metadata only (progressive disclosure tier 1)
|
|
536
|
+
const metadata = visible.map(({ content: _, ...rest }) => rest);
|
|
537
|
+
return { content: [{ type: "text", text: JSON.stringify(metadata, null, 2) }] };
|
|
538
|
+
});
|
|
539
|
+
server.tool("get_skill", "Get a skill's full content by ID (progressive disclosure tier 2). Returns content + file list.", GetSkillSchema.shape, async (args) => {
|
|
540
|
+
const skill = await store.getSkill(args.skill_id);
|
|
541
|
+
if (!skill)
|
|
542
|
+
return ACCESS_DENIED;
|
|
543
|
+
// Check agent visibility (agents[] + selector)
|
|
544
|
+
const agent = await store.getAgent(agentId);
|
|
545
|
+
if (agent && skill.agents.length > 0) {
|
|
546
|
+
if (!skill.agents.some((a) => a.toLowerCase() === agent.name.toLowerCase())) {
|
|
547
|
+
return ACCESS_DENIED;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (agent && skill.selector) {
|
|
551
|
+
if (skill.selector.type && skill.selector.type.toLowerCase() !== agent.type.toLowerCase())
|
|
552
|
+
return ACCESS_DENIED;
|
|
553
|
+
if (skill.selector.capability && !agent.capabilities.some((c) => c.toLowerCase() === skill.selector.capability.toLowerCase()))
|
|
554
|
+
return ACCESS_DENIED;
|
|
555
|
+
}
|
|
556
|
+
return { content: [{ type: "text", text: JSON.stringify(skill, null, 2) }] };
|
|
557
|
+
});
|
|
558
|
+
server.tool("delete_skill", "Delete a skill by ID. Creator or listed agents can delete.", DeleteSkillSchema.shape, async (args) => {
|
|
559
|
+
const skill = await store.getSkill(args.skill_id);
|
|
560
|
+
if (!skill)
|
|
561
|
+
return ACCESS_DENIED;
|
|
562
|
+
// Authorization: creator OR listed agent
|
|
563
|
+
if (skill.createdBy !== agentId) {
|
|
564
|
+
const agent = await store.getAgent(agentId);
|
|
565
|
+
const agentName = agent?.name ?? "";
|
|
566
|
+
if (!skill.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
|
|
567
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can delete this skill" }) }] };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const deleted = await store.deleteSkill(args.skill_id);
|
|
571
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted }) }] };
|
|
572
|
+
});
|
|
573
|
+
// --- Skill File tools ---
|
|
574
|
+
server.tool("set_skill_file", "Add or update a file attached to a skill. Creator or listed agents can manage files.", SetSkillFileSchema.shape, async (args) => {
|
|
575
|
+
const skill = await store.getSkill(args.skill_id);
|
|
576
|
+
if (!skill)
|
|
577
|
+
return ACCESS_DENIED;
|
|
578
|
+
// Authorization: creator OR listed agent
|
|
579
|
+
if (skill.createdBy !== agentId) {
|
|
580
|
+
const agent = await store.getAgent(agentId);
|
|
581
|
+
const agentName = agent?.name ?? "";
|
|
582
|
+
if (!skill.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
|
|
583
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can manage skill files" }) }] };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const file = await store.setSkillFile(args.skill_id, args.filename, args.content);
|
|
587
|
+
return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
|
|
588
|
+
});
|
|
589
|
+
server.tool("get_skill_file", "Get a file attached to a skill (progressive disclosure tier 3).", GetSkillFileSchema.shape, async (args) => {
|
|
590
|
+
// Check parent skill visibility first
|
|
591
|
+
const skill = await store.getSkill(args.skill_id);
|
|
592
|
+
if (!skill)
|
|
593
|
+
return ACCESS_DENIED;
|
|
594
|
+
const agent = await store.getAgent(agentId);
|
|
595
|
+
if (agent && skill.agents.length > 0) {
|
|
596
|
+
if (!skill.agents.some((a) => a.toLowerCase() === agent.name.toLowerCase())) {
|
|
597
|
+
return ACCESS_DENIED;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const file = await store.getSkillFile(args.skill_id, args.filename);
|
|
601
|
+
if (!file) {
|
|
602
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }] };
|
|
603
|
+
}
|
|
604
|
+
return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
|
|
605
|
+
});
|
|
606
|
+
// --- Agent Memory tools ---
|
|
607
|
+
server.tool("set_agent_memory", "Save private memory for yourself. No scope = global. With project_id = per-project. With conversation_id = per-conversation. Content overwrites previous.", SetAgentMemorySchema.shape, async (args) => {
|
|
608
|
+
let scope = "global";
|
|
609
|
+
let scopeId;
|
|
610
|
+
if (args.conversation_id) {
|
|
611
|
+
scope = "conversation";
|
|
612
|
+
scopeId = args.conversation_id;
|
|
613
|
+
// Verify subscribed to the conversation
|
|
614
|
+
const subscribed = await store.isSubscribed(args.conversation_id, agentId);
|
|
615
|
+
if (!subscribed)
|
|
616
|
+
return ACCESS_DENIED;
|
|
617
|
+
}
|
|
618
|
+
else if (args.project_id) {
|
|
619
|
+
scope = "project";
|
|
620
|
+
scopeId = args.project_id;
|
|
621
|
+
// Verify project access
|
|
622
|
+
const project = await store.getProject(args.project_id, agentId);
|
|
623
|
+
if (!project)
|
|
624
|
+
return ACCESS_DENIED;
|
|
625
|
+
}
|
|
626
|
+
const mem = await store.setAgentMemory(agentId, scope, args.content, scopeId);
|
|
627
|
+
return { content: [{ type: "text", text: JSON.stringify(mem, null, 2) }] };
|
|
628
|
+
});
|
|
629
|
+
server.tool("get_agent_memory", "Read your private memory. No scope = global. With project_id = per-project. With conversation_id = per-conversation.", GetAgentMemorySchema.shape, async (args) => {
|
|
630
|
+
let scope = "global";
|
|
631
|
+
let scopeId;
|
|
632
|
+
if (args.conversation_id) {
|
|
633
|
+
scope = "conversation";
|
|
634
|
+
scopeId = args.conversation_id;
|
|
635
|
+
}
|
|
636
|
+
else if (args.project_id) {
|
|
637
|
+
scope = "project";
|
|
638
|
+
scopeId = args.project_id;
|
|
639
|
+
}
|
|
640
|
+
const mem = await store.getAgentMemory(agentId, scope, scopeId);
|
|
641
|
+
if (!mem) {
|
|
642
|
+
return { content: [{ type: "text", text: JSON.stringify({ content: null, scope }) }] };
|
|
643
|
+
}
|
|
644
|
+
return { content: [{ type: "text", text: JSON.stringify(mem, null, 2) }] };
|
|
645
|
+
});
|
|
646
|
+
server.tool("delete_agent_memory", "Delete your private memory for a scope. No scope = global. With project_id = per-project. With conversation_id = per-conversation.", DeleteAgentMemorySchema.shape, async (args) => {
|
|
647
|
+
let scope = "global";
|
|
648
|
+
let scopeId;
|
|
649
|
+
if (args.conversation_id) {
|
|
650
|
+
scope = "conversation";
|
|
651
|
+
scopeId = args.conversation_id;
|
|
652
|
+
}
|
|
653
|
+
else if (args.project_id) {
|
|
654
|
+
scope = "project";
|
|
655
|
+
scopeId = args.project_id;
|
|
656
|
+
}
|
|
657
|
+
const deleted = await store.deleteAgentMemory(agentId, scope, scopeId);
|
|
658
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted, scope }) }] };
|
|
659
|
+
});
|
|
660
|
+
// --- Status tools ---
|
|
315
661
|
server.tool("get_status", "Get a summary: projects, active conversations, unread messages, online agents", GetStatusSchema.shape, async () => {
|
|
316
662
|
const [projects, agents, unreadCount] = await Promise.all([
|
|
317
663
|
store.listProjects(agentId),
|
|
@@ -535,6 +881,8 @@ export async function startBridgeServer(opts) {
|
|
|
535
881
|
if (eventBusListeners && store.eventBus) {
|
|
536
882
|
store.eventBus.offMessage(eventBusListeners.messageListener);
|
|
537
883
|
store.eventBus.offAccessRequest(eventBusListeners.accessRequestListener);
|
|
884
|
+
store.eventBus.offTaskCreated(eventBusListeners.taskCreatedListener);
|
|
885
|
+
store.eventBus.offTaskUpdated(eventBusListeners.taskUpdatedListener);
|
|
538
886
|
}
|
|
539
887
|
// Close all active transports (best-effort — some may already be closed)
|
|
540
888
|
for (const transport of transports.values()) {
|
|
@@ -587,10 +935,22 @@ function setupSSEDispatch(store) {
|
|
|
587
935
|
log.error(`SSE access-request dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
588
936
|
});
|
|
589
937
|
};
|
|
938
|
+
const taskCreatedListener = (event) => {
|
|
939
|
+
dispatchTaskNotification(event.task, "created").catch((err) => {
|
|
940
|
+
log.error(`SSE task dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
const taskUpdatedListener = (event) => {
|
|
944
|
+
dispatchTaskNotification(event.task, event.action).catch((err) => {
|
|
945
|
+
log.error(`SSE task dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
946
|
+
});
|
|
947
|
+
};
|
|
590
948
|
store.eventBus.onMessage(messageListener);
|
|
591
949
|
store.eventBus.onAccessRequest(accessRequestListener);
|
|
950
|
+
store.eventBus.onTaskCreated(taskCreatedListener);
|
|
951
|
+
store.eventBus.onTaskUpdated(taskUpdatedListener);
|
|
592
952
|
log.info("SSE push notifications enabled");
|
|
593
|
-
return { messageListener, accessRequestListener };
|
|
953
|
+
return { messageListener, accessRequestListener, taskCreatedListener, taskUpdatedListener };
|
|
594
954
|
}
|
|
595
955
|
async function dispatchMessageNotification(store, event) {
|
|
596
956
|
const { message } = event;
|
|
@@ -630,6 +990,9 @@ async function dispatchMessageNotification(store, event) {
|
|
|
630
990
|
const agentVisInt = VISIBILITY_ORDER[agent.clearanceLevel];
|
|
631
991
|
if (agentVisInt < messageVisInt)
|
|
632
992
|
continue;
|
|
993
|
+
// Whisper gate: if message has recipients, agent must be in the list
|
|
994
|
+
if (message.recipients && !message.recipients.includes(sub.agentId))
|
|
995
|
+
continue;
|
|
633
996
|
// Find all active sessions for this agent and push notification
|
|
634
997
|
const sessions = agentSessions.get(sub.agentId);
|
|
635
998
|
if (!sessions)
|
|
@@ -680,4 +1043,35 @@ async function dispatchAccessRequestNotification(store, event) {
|
|
|
680
1043
|
}
|
|
681
1044
|
}
|
|
682
1045
|
}
|
|
1046
|
+
async function dispatchTaskNotification(task, action) {
|
|
1047
|
+
const notification = {
|
|
1048
|
+
jsonrpc: "2.0",
|
|
1049
|
+
method: "notifications/task",
|
|
1050
|
+
params: {
|
|
1051
|
+
taskId: task.id,
|
|
1052
|
+
projectId: task.projectId,
|
|
1053
|
+
conversationId: task.conversationId,
|
|
1054
|
+
title: task.title,
|
|
1055
|
+
status: task.status,
|
|
1056
|
+
action,
|
|
1057
|
+
createdBy: task.createdBy,
|
|
1058
|
+
claimedBy: task.claimedBy,
|
|
1059
|
+
updatedAt: task.updatedAt,
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
// Project-level notification: push to all agents with active sessions
|
|
1063
|
+
for (const [, sessions] of agentSessions) {
|
|
1064
|
+
for (const sessionId of sessions) {
|
|
1065
|
+
const transport = transports.get(sessionId);
|
|
1066
|
+
if (!transport)
|
|
1067
|
+
continue;
|
|
1068
|
+
try {
|
|
1069
|
+
await transport.send(notification);
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
log.debug(`SSE task send failed for session ${sessionId}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
683
1077
|
//# sourceMappingURL=server.js.map
|