mcp-agent-broker 1.0.0 → 1.0.2
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 +59 -13
- package/dist/app.js +0 -1
- package/dist/bin.js +108 -0
- package/dist/infrastructure/db/schema.js +11 -1
- package/dist/modules/identity/agent.service.js +3 -3
- package/dist/modules/task/task.service.js +69 -0
- package/dist/transport/mcp/index.js +91 -2
- package/package.json +5 -4
- package/seed_api_key.ts +30 -0
- package/docker-compose.yml +0 -41
- package/drizzle.config.ts +0 -10
- package/src/app.ts +0 -31
- package/src/infrastructure/db/index.ts +0 -10
- package/src/infrastructure/db/migrations/0000_flawless_vanisher.sql +0 -103
- package/src/infrastructure/db/migrations/0001_fearless_sersi.sql +0 -36
- package/src/infrastructure/db/migrations/meta/0000_snapshot.json +0 -714
- package/src/infrastructure/db/migrations/meta/0001_snapshot.json +0 -947
- package/src/infrastructure/db/migrations/meta/_journal.json +0 -20
- package/src/infrastructure/db/schema.ts +0 -148
- package/src/infrastructure/redis/index.ts +0 -7
- package/src/modules/event/event.service.ts +0 -50
- package/src/modules/identity/agent.service.ts +0 -58
- package/src/modules/lease/lease.service.ts +0 -89
- package/src/modules/task/task.service.ts +0 -46
- package/src/transport/mcp/index.ts +0 -167
- package/src/workers/outbox-relay.ts +0 -61
- package/test_client.ts +0 -34
- package/test_task.ts +0 -68
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,31 +1,77 @@
|
|
|
1
1
|
# MCP Agent Broker
|
|
2
2
|
|
|
3
|
-
The Model Context Protocol (MCP) Agent Broker is a centralized service designed to orchestrate and manage autonomous coding agents.
|
|
3
|
+
The Model Context Protocol (MCP) Agent Broker is a centralized service designed to orchestrate and manage autonomous coding agents.
|
|
4
|
+
|
|
5
|
+
It acts as a middleware that securely maps AI Agents (Principals) to tasks, enforcing distributed locks (Fencing Tokens) so multiple agents don't step on each other's toes when working on the same repository.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
* **MCP Server Integration:** Natively implements the MCP SDK via Server-Sent Events (SSE).
|
|
9
|
+
* **Secure Authentication:** Validates API keys against securely hashed database records.
|
|
7
10
|
* **Distributed Locking:** Safely hand off tasks between implementer agents and reviewer agents using fencing tokens.
|
|
8
11
|
* **Transactional Outbox:** Guaranteed delivery of domain events without dual-write issues.
|
|
9
12
|
* **Event Relay:** Real-time event broadcasting over Redis Pub/Sub.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* Redis
|
|
15
|
-
* MinIO
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
### 1. Start the Infrastructure
|
|
19
|
+
The broker requires PostgreSQL and Redis. We provide a `docker-compose.yml` to spin up everything (including the broker itself) instantly.
|
|
18
20
|
|
|
19
|
-
1. Start the infrastructure:
|
|
20
21
|
```bash
|
|
21
22
|
docker compose up -d
|
|
22
23
|
```
|
|
24
|
+
*Note: If you are doing local development, you can run `docker compose up -d postgres redis` and then run the app locally using `npm run dev`.*
|
|
25
|
+
|
|
26
|
+
### 2. Generate an API Key
|
|
27
|
+
To connect to the broker, your Agent must have an API Key. We provide a seed script to generate your first Workspace, Principal, and API Key.
|
|
23
28
|
|
|
24
|
-
2. Run the server:
|
|
25
29
|
```bash
|
|
26
|
-
|
|
30
|
+
npx tsx seed_api_key.ts
|
|
27
31
|
```
|
|
28
|
-
|
|
32
|
+
*Take note of the API Key (e.g. `test-api-key-123`) output by the terminal.*
|
|
33
|
+
|
|
34
|
+
### 3. Connect your Agents
|
|
35
|
+
Point your MCP-compatible client to the broker's endpoint and pass your API key as a Bearer token.
|
|
36
|
+
|
|
37
|
+
**Programmatic Example (TypeScript):**
|
|
38
|
+
```typescript
|
|
39
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
40
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
41
|
+
|
|
42
|
+
const transport = new SSEClientTransport(new URL("http://localhost:3000/mcp/sse"), {
|
|
43
|
+
eventSourceInit: {
|
|
44
|
+
headers: { Authorization: "Bearer test-api-key-123" }
|
|
45
|
+
} as any,
|
|
46
|
+
requestInit: {
|
|
47
|
+
headers: { Authorization: "Bearer test-api-key-123" }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const client = new Client({ name: "my-agent", version: "1.0.0" }, { capabilities: {} });
|
|
52
|
+
await client.connect(transport);
|
|
53
|
+
|
|
54
|
+
// Register your agent!
|
|
55
|
+
await client.request({
|
|
56
|
+
method: "tools/call",
|
|
57
|
+
params: {
|
|
58
|
+
name: "register_agent",
|
|
59
|
+
arguments: {
|
|
60
|
+
workspaceId: "00000000-0000-0000-0000-000000000000",
|
|
61
|
+
name: "My Implementer Agent",
|
|
62
|
+
version: "1.0",
|
|
63
|
+
capabilities: ["execute"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, CallToolResultSchema);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Listen to Real-Time Events
|
|
70
|
+
Any downstream CI dashboard or review agent can connect to the Redis server and subscribe to `workspace:{workspace_id}:events` to see real-time updates as tasks are created and leases are acquired!
|
|
71
|
+
|
|
72
|
+
---
|
|
29
73
|
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
## Future Improvements for Contributors
|
|
75
|
+
* Implement standard OAuth2 / OIDC for user-facing dashboards.
|
|
76
|
+
* Extend the `Review Module` to enforce reviewer sign-offs before pushing to main branches.
|
|
77
|
+
* Implement the Fencing Token check in a Git Hook or CI/CD layer.
|
package/dist/app.js
CHANGED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
38
|
+
const readline = __importStar(require("readline"));
|
|
39
|
+
async function runProxy(url, apiKey) {
|
|
40
|
+
const transport = new sse_js_1.SSEClientTransport(new URL(url), {
|
|
41
|
+
eventSourceInit: {
|
|
42
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
43
|
+
},
|
|
44
|
+
requestInit: {
|
|
45
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
transport.onmessage = (message) => {
|
|
49
|
+
process.stdout.write(JSON.stringify(message) + "\n");
|
|
50
|
+
};
|
|
51
|
+
transport.onerror = (error) => {
|
|
52
|
+
console.error("Transport error:", error);
|
|
53
|
+
};
|
|
54
|
+
transport.onclose = () => {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
};
|
|
57
|
+
await transport.start();
|
|
58
|
+
const rl = readline.createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
terminal: false
|
|
61
|
+
});
|
|
62
|
+
rl.on("line", async (line) => {
|
|
63
|
+
if (!line.trim())
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
const message = JSON.parse(line);
|
|
67
|
+
await transport.send(message);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.error("Error processing line:", e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
rl.on("close", () => {
|
|
74
|
+
transport.close().catch(() => { });
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const args = process.argv.slice(2);
|
|
79
|
+
const command = args[0];
|
|
80
|
+
if (command === "connect") {
|
|
81
|
+
let url = "http://127.0.0.1:3000/mcp/sse";
|
|
82
|
+
let apiKey = "";
|
|
83
|
+
for (let i = 1; i < args.length; i++) {
|
|
84
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
85
|
+
url = args[i + 1];
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
else if (args[i] === "--token" && args[i + 1]) {
|
|
89
|
+
apiKey = args[i + 1];
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
console.error("Missing --token");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
runProxy(url, apiKey).catch((e) => {
|
|
98
|
+
console.error("Proxy error:", e);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Start server
|
|
104
|
+
import("./app.js").catch(e => {
|
|
105
|
+
console.error("Failed to start server:", e);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.idempotencyRecords = exports.outbox = exports.events = exports.leases = exports.taskAttempts = exports.tasks = exports.agentInstances = exports.agentDefinitions = exports.workspaces = exports.leaseTypeEnum = exports.leaseStatusEnum = exports.agentStatusEnum = exports.workspaceStatusEnum = exports.taskStateEnum = void 0;
|
|
3
|
+
exports.idempotencyRecords = exports.outbox = exports.events = exports.leases = exports.taskAttempts = exports.tasks = exports.agentInstances = exports.agentDefinitions = exports.apiKeys = exports.principals = exports.workspaces = exports.leaseTypeEnum = exports.leaseStatusEnum = exports.agentStatusEnum = exports.workspaceStatusEnum = exports.taskStateEnum = void 0;
|
|
4
4
|
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
5
5
|
exports.taskStateEnum = (0, pg_core_1.pgEnum)('task_state', [
|
|
6
6
|
'draft', 'ready', 'leased', 'running', 'blocked',
|
|
@@ -19,6 +19,16 @@ exports.workspaces = (0, pg_core_1.pgTable)('workspaces', {
|
|
|
19
19
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
20
20
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
21
21
|
});
|
|
22
|
+
exports.principals = (0, pg_core_1.pgTable)('principals', {
|
|
23
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
24
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
25
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
26
|
+
});
|
|
27
|
+
exports.apiKeys = (0, pg_core_1.pgTable)('api_keys', {
|
|
28
|
+
keyHash: (0, pg_core_1.text)('key_hash').primaryKey(),
|
|
29
|
+
principalId: (0, pg_core_1.uuid)('principal_id').notNull().references(() => exports.principals.id),
|
|
30
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
31
|
+
});
|
|
22
32
|
exports.agentDefinitions = (0, pg_core_1.pgTable)('agent_definitions', {
|
|
23
33
|
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
24
34
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
@@ -17,7 +17,7 @@ class AgentService {
|
|
|
17
17
|
return workspace;
|
|
18
18
|
}
|
|
19
19
|
async registerAgent(params) {
|
|
20
|
-
const workspace = await this.ensureDefaultWorkspace();
|
|
20
|
+
const workspace = await this.ensureDefaultWorkspace(); // Fallback logic or just rely on params
|
|
21
21
|
// 1. Create or find agent definition
|
|
22
22
|
let [definition] = await index_js_1.db.select().from(schema_js_1.agentDefinitions)
|
|
23
23
|
.where((0, drizzle_orm_1.eq)(schema_js_1.agentDefinitions.name, params.name))
|
|
@@ -44,8 +44,8 @@ class AgentService {
|
|
|
44
44
|
const [instance] = await index_js_1.db.insert(schema_js_1.agentInstances).values({
|
|
45
45
|
id: (0, crypto_1.randomUUID)(),
|
|
46
46
|
agentDefinitionId: definition.id,
|
|
47
|
-
workspaceId:
|
|
48
|
-
principalId:
|
|
47
|
+
workspaceId: params.workspaceId,
|
|
48
|
+
principalId: params.principalId,
|
|
49
49
|
capabilitiesJson: params.capabilities,
|
|
50
50
|
status: "connected",
|
|
51
51
|
}).returning();
|
|
@@ -4,6 +4,7 @@ exports.taskService = exports.TaskService = void 0;
|
|
|
4
4
|
const index_js_1 = require("../../infrastructure/db/index.js");
|
|
5
5
|
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
7
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
7
8
|
const event_service_js_1 = require("../event/event.service.js");
|
|
8
9
|
class TaskService {
|
|
9
10
|
async createTask(params) {
|
|
@@ -35,6 +36,74 @@ class TaskService {
|
|
|
35
36
|
return task;
|
|
36
37
|
});
|
|
37
38
|
}
|
|
39
|
+
async submitImplementation(params) {
|
|
40
|
+
return await index_js_1.db.transaction(async (tx) => {
|
|
41
|
+
const [task] = await tx.select().from(schema_js_1.tasks).where((0, drizzle_orm_1.eq)(schema_js_1.tasks.id, params.taskId)).for('update');
|
|
42
|
+
if (!task)
|
|
43
|
+
throw new Error('Task not found');
|
|
44
|
+
if (task.nextFencingToken !== params.fencingToken) {
|
|
45
|
+
throw new Error('Invalid fencing token. You no longer hold the active lease.');
|
|
46
|
+
}
|
|
47
|
+
const [updatedTask] = await tx.update(schema_js_1.tasks).set({
|
|
48
|
+
state: 'awaiting_review',
|
|
49
|
+
activeExecutionLeaseId: null,
|
|
50
|
+
version: task.version + 1,
|
|
51
|
+
updatedAt: new Date()
|
|
52
|
+
}).where((0, drizzle_orm_1.eq)(schema_js_1.tasks.id, params.taskId)).returning();
|
|
53
|
+
if (task.activeExecutionLeaseId) {
|
|
54
|
+
await tx.update(schema_js_1.leases).set({
|
|
55
|
+
status: 'released',
|
|
56
|
+
releasedAt: new Date()
|
|
57
|
+
}).where((0, drizzle_orm_1.eq)(schema_js_1.leases.id, task.activeExecutionLeaseId));
|
|
58
|
+
}
|
|
59
|
+
await event_service_js_1.eventService.appendEvent(tx, {
|
|
60
|
+
workspaceId: task.workspaceId,
|
|
61
|
+
eventType: 'ImplementationSubmitted',
|
|
62
|
+
aggregateType: 'Task',
|
|
63
|
+
aggregateId: task.id,
|
|
64
|
+
aggregateVersion: updatedTask.version,
|
|
65
|
+
actorType: 'Agent',
|
|
66
|
+
actorId: params.agentInstanceId,
|
|
67
|
+
payload: { pullRequestUrl: params.pullRequestUrl }
|
|
68
|
+
});
|
|
69
|
+
return updatedTask;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async submitReview(params) {
|
|
73
|
+
return await index_js_1.db.transaction(async (tx) => {
|
|
74
|
+
const [task] = await tx.select().from(schema_js_1.tasks).where((0, drizzle_orm_1.eq)(schema_js_1.tasks.id, params.taskId)).for('update');
|
|
75
|
+
if (!task)
|
|
76
|
+
throw new Error('Task not found');
|
|
77
|
+
if (task.nextFencingToken !== params.fencingToken) {
|
|
78
|
+
throw new Error('Invalid fencing token. You no longer hold the active lease.');
|
|
79
|
+
}
|
|
80
|
+
const newState = params.status === 'approved' ? 'completed' : 'changes_requested';
|
|
81
|
+
const [updatedTask] = await tx.update(schema_js_1.tasks).set({
|
|
82
|
+
state: newState,
|
|
83
|
+
activeReviewLeaseId: null,
|
|
84
|
+
version: task.version + 1,
|
|
85
|
+
completedAt: params.status === 'approved' ? new Date() : null,
|
|
86
|
+
updatedAt: new Date()
|
|
87
|
+
}).where((0, drizzle_orm_1.eq)(schema_js_1.tasks.id, params.taskId)).returning();
|
|
88
|
+
if (task.activeReviewLeaseId) {
|
|
89
|
+
await tx.update(schema_js_1.leases).set({
|
|
90
|
+
status: 'released',
|
|
91
|
+
releasedAt: new Date()
|
|
92
|
+
}).where((0, drizzle_orm_1.eq)(schema_js_1.leases.id, task.activeReviewLeaseId));
|
|
93
|
+
}
|
|
94
|
+
await event_service_js_1.eventService.appendEvent(tx, {
|
|
95
|
+
workspaceId: task.workspaceId,
|
|
96
|
+
eventType: 'ReviewSubmitted',
|
|
97
|
+
aggregateType: 'Task',
|
|
98
|
+
aggregateId: task.id,
|
|
99
|
+
aggregateVersion: updatedTask.version,
|
|
100
|
+
actorType: 'Agent',
|
|
101
|
+
actorId: params.agentInstanceId,
|
|
102
|
+
payload: { status: params.status, feedback: params.feedback }
|
|
103
|
+
});
|
|
104
|
+
return updatedTask;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
38
107
|
}
|
|
39
108
|
exports.TaskService = TaskService;
|
|
40
109
|
exports.taskService = new TaskService();
|
|
@@ -4,6 +4,10 @@ exports.setupMcpTransport = setupMcpTransport;
|
|
|
4
4
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
5
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
6
6
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
9
|
+
const index_js_2 = require("../../infrastructure/db/index.js");
|
|
10
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
7
11
|
const agent_service_js_1 = require("../../modules/identity/agent.service.js");
|
|
8
12
|
const task_service_js_1 = require("../../modules/task/task.service.js");
|
|
9
13
|
const lease_service_js_1 = require("../../modules/lease/lease.service.js");
|
|
@@ -11,6 +15,19 @@ async function setupMcpTransport(fastify) {
|
|
|
11
15
|
// Store transports for the POST /mcp/message route
|
|
12
16
|
const transports = new Map();
|
|
13
17
|
fastify.get("/mcp/sse", async (request, reply) => {
|
|
18
|
+
const authHeader = request.headers.authorization;
|
|
19
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
20
|
+
reply.status(401).send({ error: "Missing or invalid Authorization header" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const token = authHeader.split(' ')[1];
|
|
24
|
+
const keyHash = (0, crypto_1.createHash)('sha256').update(token).digest('hex');
|
|
25
|
+
const [apiKeyRecord] = await index_js_2.db.select().from(schema_js_1.apiKeys).where((0, drizzle_orm_1.eq)(schema_js_1.apiKeys.keyHash, keyHash));
|
|
26
|
+
if (!apiKeyRecord) {
|
|
27
|
+
reply.status(401).send({ error: "Invalid API Key" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const principalId = apiKeyRecord.principalId;
|
|
14
31
|
// Hijack the Fastify response so we can stream SSE headers manually
|
|
15
32
|
reply.hijack();
|
|
16
33
|
const transport = new sse_js_1.SSEServerTransport("/mcp/message", reply.raw);
|
|
@@ -67,6 +84,35 @@ async function setupMcpTransport(fastify) {
|
|
|
67
84
|
},
|
|
68
85
|
required: ["taskId", "workspaceId", "agentInstanceId", "leaseType"]
|
|
69
86
|
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "submit_task_implementation",
|
|
90
|
+
description: "Submit task implementation for review",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
taskId: { type: "string" },
|
|
95
|
+
agentInstanceId: { type: "string" },
|
|
96
|
+
fencingToken: { type: "number" },
|
|
97
|
+
pullRequestUrl: { type: "string" }
|
|
98
|
+
},
|
|
99
|
+
required: ["taskId", "agentInstanceId", "fencingToken"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "submit_task_review",
|
|
104
|
+
description: "Submit a review (approve/reject) for an implemented task",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
taskId: { type: "string" },
|
|
109
|
+
agentInstanceId: { type: "string" },
|
|
110
|
+
fencingToken: { type: "number" },
|
|
111
|
+
status: { type: "string", enum: ["approved", "rejected"] },
|
|
112
|
+
feedback: { type: "string" }
|
|
113
|
+
},
|
|
114
|
+
required: ["taskId", "agentInstanceId", "fencingToken", "status"]
|
|
115
|
+
}
|
|
70
116
|
}
|
|
71
117
|
]
|
|
72
118
|
};
|
|
@@ -75,7 +121,9 @@ async function setupMcpTransport(fastify) {
|
|
|
75
121
|
if (req.params.name === "register_agent") {
|
|
76
122
|
try {
|
|
77
123
|
const args = req.params.arguments;
|
|
78
|
-
const
|
|
124
|
+
const agentInstance = await agent_service_js_1.agentService.registerAgent({
|
|
125
|
+
workspaceId: args.workspaceId || (0, crypto_1.randomUUID)(), // Temporary fallback
|
|
126
|
+
principalId: principalId,
|
|
79
127
|
name: args.name,
|
|
80
128
|
version: args.version,
|
|
81
129
|
capabilities: args.capabilities,
|
|
@@ -84,7 +132,7 @@ async function setupMcpTransport(fastify) {
|
|
|
84
132
|
content: [
|
|
85
133
|
{
|
|
86
134
|
type: "text",
|
|
87
|
-
text: `Agent registered successfully! Instance ID: ${
|
|
135
|
+
text: `Agent registered successfully! Instance ID: ${agentInstance.id}`
|
|
88
136
|
}
|
|
89
137
|
]
|
|
90
138
|
};
|
|
@@ -135,6 +183,47 @@ async function setupMcpTransport(fastify) {
|
|
|
135
183
|
throw e;
|
|
136
184
|
}
|
|
137
185
|
}
|
|
186
|
+
else if (req.params.name === "submit_task_implementation") {
|
|
187
|
+
try {
|
|
188
|
+
const args = req.params.arguments;
|
|
189
|
+
const task = await task_service_js_1.taskService.submitImplementation({
|
|
190
|
+
taskId: args.taskId,
|
|
191
|
+
agentInstanceId: args.agentInstanceId,
|
|
192
|
+
fencingToken: args.fencingToken,
|
|
193
|
+
pullRequestUrl: args.pullRequestUrl
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{ type: "text", text: `Implementation submitted successfully! Task state is now: ${task.state}` }
|
|
198
|
+
]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error("Error in submit_task_implementation:", e);
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (req.params.name === "submit_task_review") {
|
|
207
|
+
try {
|
|
208
|
+
const args = req.params.arguments;
|
|
209
|
+
const task = await task_service_js_1.taskService.submitReview({
|
|
210
|
+
taskId: args.taskId,
|
|
211
|
+
agentInstanceId: args.agentInstanceId,
|
|
212
|
+
fencingToken: args.fencingToken,
|
|
213
|
+
status: args.status,
|
|
214
|
+
feedback: args.feedback
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{ type: "text", text: `Review submitted successfully! Task state is now: ${task.state}` }
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
console.error("Error in submit_task_review:", e);
|
|
224
|
+
throw e;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
138
227
|
throw new Error(`Tool not found: ${req.params.name}`);
|
|
139
228
|
});
|
|
140
229
|
await mcpServer.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-agent-broker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "dist/app.js",
|
|
5
5
|
"bin": {
|
|
6
|
-
"mcp-agent-broker": "dist/
|
|
6
|
+
"mcp-agent-broker": "dist/bin.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"start": "node dist/app.js",
|
|
11
11
|
"dev": "tsx watch src/app.ts",
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "vitest run"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [],
|
|
15
15
|
"author": "",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@types/pg": "^8.20.0",
|
|
31
31
|
"drizzle-kit": "^0.31.10",
|
|
32
32
|
"tsx": "^4.23.1",
|
|
33
|
-
"typescript": "^7.0.2"
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vitest": "^4.1.10"
|
|
34
35
|
}
|
|
35
36
|
}
|
package/seed_api_key.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { db } from './src/infrastructure/db/index.js';
|
|
2
|
+
import { principals, apiKeys, workspaces } from './src/infrastructure/db/schema.js';
|
|
3
|
+
import { randomUUID, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
async function seed() {
|
|
6
|
+
const principalId = randomUUID();
|
|
7
|
+
const rawKey = 'test-api-key-123';
|
|
8
|
+
const keyHash = createHash('sha256').update(rawKey).digest('hex');
|
|
9
|
+
|
|
10
|
+
const workspaceId = '00000000-0000-0000-0000-000000000000';
|
|
11
|
+
await db.insert(workspaces).values({
|
|
12
|
+
id: workspaceId,
|
|
13
|
+
name: 'Test Workspace',
|
|
14
|
+
}).onConflictDoNothing();
|
|
15
|
+
|
|
16
|
+
await db.insert(principals).values({
|
|
17
|
+
id: principalId,
|
|
18
|
+
name: 'Test Service Account',
|
|
19
|
+
}).onConflictDoNothing();
|
|
20
|
+
|
|
21
|
+
await db.insert(apiKeys).values({
|
|
22
|
+
keyHash,
|
|
23
|
+
principalId,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log('Seeded principal:', principalId);
|
|
27
|
+
console.log('API Key:', rawKey);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
seed().catch(console.error);
|
package/docker-compose.yml
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
postgres:
|
|
3
|
-
image: postgres:16-alpine
|
|
4
|
-
container_name: mab-postgres
|
|
5
|
-
environment:
|
|
6
|
-
POSTGRES_USER: mab_user
|
|
7
|
-
POSTGRES_PASSWORD: mab_password
|
|
8
|
-
POSTGRES_DB: mab_db
|
|
9
|
-
ports:
|
|
10
|
-
- "5433:5432"
|
|
11
|
-
volumes:
|
|
12
|
-
- postgres_data:/var/lib/postgresql/data
|
|
13
|
-
restart: unless-stopped
|
|
14
|
-
|
|
15
|
-
redis:
|
|
16
|
-
image: redis:7-alpine
|
|
17
|
-
container_name: mab-redis
|
|
18
|
-
ports:
|
|
19
|
-
- "6379:6379"
|
|
20
|
-
volumes:
|
|
21
|
-
- redis_data:/data
|
|
22
|
-
restart: unless-stopped
|
|
23
|
-
|
|
24
|
-
minio:
|
|
25
|
-
image: minio/minio:latest
|
|
26
|
-
container_name: mab-minio
|
|
27
|
-
environment:
|
|
28
|
-
MINIO_ROOT_USER: mab_admin
|
|
29
|
-
MINIO_ROOT_PASSWORD: mab_password123
|
|
30
|
-
command: server /data --console-address ":9001"
|
|
31
|
-
ports:
|
|
32
|
-
- "9000:9000"
|
|
33
|
-
- "9001:9001"
|
|
34
|
-
volumes:
|
|
35
|
-
- minio_data:/data
|
|
36
|
-
restart: unless-stopped
|
|
37
|
-
|
|
38
|
-
volumes:
|
|
39
|
-
postgres_data:
|
|
40
|
-
redis_data:
|
|
41
|
-
minio_data:
|
package/drizzle.config.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "drizzle-kit";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
schema: "./src/infrastructure/db/schema.ts",
|
|
5
|
-
out: "./src/infrastructure/db/migrations",
|
|
6
|
-
dialect: "postgresql",
|
|
7
|
-
dbCredentials: {
|
|
8
|
-
url: process.env.DATABASE_URL || "postgres://mab_user:mab_password@127.0.0.1:5433/mab_db"
|
|
9
|
-
}
|
|
10
|
-
});
|
package/src/app.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import Fastify from 'fastify';
|
|
3
|
-
import { setupMcpTransport } from './transport/mcp/index.js';
|
|
4
|
-
import { db } from './infrastructure/db/index.js';
|
|
5
|
-
import { outboxRelay } from './workers/outbox-relay.js';
|
|
6
|
-
|
|
7
|
-
const fastify = Fastify({
|
|
8
|
-
logger: true
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
fastify.get('/health', async (request, reply) => {
|
|
12
|
-
return { status: 'ok', service: 'mcp-agent-broker' };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Start the server
|
|
16
|
-
const start = async () => {
|
|
17
|
-
try {
|
|
18
|
-
await setupMcpTransport(fastify);
|
|
19
|
-
|
|
20
|
-
// Start background workers
|
|
21
|
-
outboxRelay.start();
|
|
22
|
-
|
|
23
|
-
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
|
24
|
-
fastify.log.info(`Server listening on ${fastify.server.address()}`);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
fastify.log.error(err);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
start();
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
-
import { Pool } from 'pg';
|
|
3
|
-
import * as schema from './schema';
|
|
4
|
-
|
|
5
|
-
// Use a connection pool for the Fastify app
|
|
6
|
-
const pool = new Pool({
|
|
7
|
-
connectionString: process.env.DATABASE_URL || "postgres://mab_user:mab_password@127.0.0.1:5433/mab_db"
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const db = drizzle(pool, { schema });
|