mcp-agent-broker 1.0.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 +31 -0
- package/dist/app.js +30 -0
- package/dist/infrastructure/db/index.js +44 -0
- package/dist/infrastructure/db/schema.js +136 -0
- package/dist/infrastructure/redis/index.js +11 -0
- package/dist/modules/event/event.service.js +36 -0
- package/dist/modules/identity/agent.service.js +56 -0
- package/dist/modules/lease/lease.service.js +76 -0
- package/dist/modules/task/task.service.js +40 -0
- package/dist/transport/mcp/index.js +157 -0
- package/dist/workers/outbox-relay.js +57 -0
- package/docker-compose.yml +41 -0
- package/drizzle.config.ts +10 -0
- package/package.json +35 -0
- package/src/app.ts +31 -0
- package/src/infrastructure/db/index.ts +10 -0
- package/src/infrastructure/db/migrations/0000_flawless_vanisher.sql +103 -0
- package/src/infrastructure/db/migrations/0001_fearless_sersi.sql +36 -0
- package/src/infrastructure/db/migrations/meta/0000_snapshot.json +714 -0
- package/src/infrastructure/db/migrations/meta/0001_snapshot.json +947 -0
- package/src/infrastructure/db/migrations/meta/_journal.json +20 -0
- package/src/infrastructure/db/schema.ts +148 -0
- package/src/infrastructure/redis/index.ts +7 -0
- package/src/modules/event/event.service.ts +50 -0
- package/src/modules/identity/agent.service.ts +58 -0
- package/src/modules/lease/lease.service.ts +89 -0
- package/src/modules/task/task.service.ts +46 -0
- package/src/transport/mcp/index.ts +167 -0
- package/src/workers/outbox-relay.ts +61 -0
- package/test_client.ts +34 -0
- package/test_task.ts +68 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# MCP Agent Broker
|
|
2
|
+
|
|
3
|
+
The Model Context Protocol (MCP) Agent Broker is a centralized service designed to orchestrate and manage autonomous coding agents.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
* **MCP Server Integration:** Natively implements the MCP SDK via Server-Sent Events (SSE).
|
|
7
|
+
* **Distributed Locking:** Safely hand off tasks between implementer agents and reviewer agents using fencing tokens.
|
|
8
|
+
* **Transactional Outbox:** Guaranteed delivery of domain events without dual-write issues.
|
|
9
|
+
* **Event Relay:** Real-time event broadcasting over Redis Pub/Sub.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
You will need Docker installed to run the dependencies:
|
|
13
|
+
* PostgreSQL
|
|
14
|
+
* Redis
|
|
15
|
+
* MinIO
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
1. Start the infrastructure:
|
|
20
|
+
```bash
|
|
21
|
+
docker compose up -d
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. Run the server:
|
|
25
|
+
```bash
|
|
26
|
+
npm start
|
|
27
|
+
```
|
|
28
|
+
The server will bind to `http://localhost:3000/mcp/sse`.
|
|
29
|
+
|
|
30
|
+
3. Connect your Agents:
|
|
31
|
+
Point your MCP-compatible client (like Claude Code) to `http://localhost:3000/mcp/sse`. They can now invoke tools like `register_agent`, `create_task`, and `acquire_task_lease`!
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
8
|
+
const index_js_1 = require("./transport/mcp/index.js");
|
|
9
|
+
const outbox_relay_js_1 = require("./workers/outbox-relay.js");
|
|
10
|
+
const fastify = (0, fastify_1.default)({
|
|
11
|
+
logger: true
|
|
12
|
+
});
|
|
13
|
+
fastify.get('/health', async (request, reply) => {
|
|
14
|
+
return { status: 'ok', service: 'mcp-agent-broker' };
|
|
15
|
+
});
|
|
16
|
+
// Start the server
|
|
17
|
+
const start = async () => {
|
|
18
|
+
try {
|
|
19
|
+
await (0, index_js_1.setupMcpTransport)(fastify);
|
|
20
|
+
// Start background workers
|
|
21
|
+
outbox_relay_js_1.outboxRelay.start();
|
|
22
|
+
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
|
23
|
+
fastify.log.info(`Server listening on ${fastify.server.address()}`);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
fastify.log.error(err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
start();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.db = void 0;
|
|
37
|
+
const node_postgres_1 = require("drizzle-orm/node-postgres");
|
|
38
|
+
const pg_1 = require("pg");
|
|
39
|
+
const schema = __importStar(require("./schema"));
|
|
40
|
+
// Use a connection pool for the Fastify app
|
|
41
|
+
const pool = new pg_1.Pool({
|
|
42
|
+
connectionString: process.env.DATABASE_URL || "postgres://mab_user:mab_password@127.0.0.1:5433/mab_db"
|
|
43
|
+
});
|
|
44
|
+
exports.db = (0, node_postgres_1.drizzle)(pool, { schema });
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
5
|
+
exports.taskStateEnum = (0, pg_core_1.pgEnum)('task_state', [
|
|
6
|
+
'draft', 'ready', 'leased', 'running', 'blocked',
|
|
7
|
+
'awaiting_review', 'changes_requested', 'completed',
|
|
8
|
+
'failed', 'canceled', 'superseded'
|
|
9
|
+
]);
|
|
10
|
+
exports.workspaceStatusEnum = (0, pg_core_1.pgEnum)('workspace_status', ['active', 'archived']);
|
|
11
|
+
exports.agentStatusEnum = (0, pg_core_1.pgEnum)('agent_status', ['connected', 'degraded', 'disconnected', 'revoked']);
|
|
12
|
+
exports.leaseStatusEnum = (0, pg_core_1.pgEnum)('lease_status', ['active', 'released', 'expired', 'revoked']);
|
|
13
|
+
exports.leaseTypeEnum = (0, pg_core_1.pgEnum)('lease_type', ['execution', 'review']);
|
|
14
|
+
exports.workspaces = (0, pg_core_1.pgTable)('workspaces', {
|
|
15
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
16
|
+
organizationId: (0, pg_core_1.uuid)('organization_id'),
|
|
17
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
18
|
+
status: (0, exports.workspaceStatusEnum)('status').notNull().default('active'),
|
|
19
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
20
|
+
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
21
|
+
});
|
|
22
|
+
exports.agentDefinitions = (0, pg_core_1.pgTable)('agent_definitions', {
|
|
23
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
24
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
25
|
+
vendor: (0, pg_core_1.text)('vendor'),
|
|
26
|
+
version: (0, pg_core_1.text)('version').notNull(),
|
|
27
|
+
integrationType: (0, pg_core_1.text)('integration_type'),
|
|
28
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
29
|
+
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
30
|
+
});
|
|
31
|
+
exports.agentInstances = (0, pg_core_1.pgTable)('agent_instances', {
|
|
32
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
33
|
+
agentDefinitionId: (0, pg_core_1.uuid)('agent_definition_id').notNull().references(() => exports.agentDefinitions.id),
|
|
34
|
+
workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull().references(() => exports.workspaces.id),
|
|
35
|
+
principalId: (0, pg_core_1.uuid)('principal_id').notNull(),
|
|
36
|
+
status: (0, exports.agentStatusEnum)('status').notNull().default('connected'),
|
|
37
|
+
capabilitiesJson: (0, pg_core_1.jsonb)('capabilities_json').notNull().default('[]'),
|
|
38
|
+
lastHeartbeatAt: (0, pg_core_1.timestamp)('last_heartbeat_at', { withTimezone: true }),
|
|
39
|
+
revokedAt: (0, pg_core_1.timestamp)('revoked_at', { withTimezone: true }),
|
|
40
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
41
|
+
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
42
|
+
});
|
|
43
|
+
exports.tasks = (0, pg_core_1.pgTable)('tasks', {
|
|
44
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
45
|
+
workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull(),
|
|
46
|
+
repositoryId: (0, pg_core_1.uuid)('repository_id').notNull(),
|
|
47
|
+
sessionId: (0, pg_core_1.uuid)('session_id'),
|
|
48
|
+
objective: (0, pg_core_1.text)('objective').notNull(),
|
|
49
|
+
description: (0, pg_core_1.text)('description').notNull().default(''),
|
|
50
|
+
acceptanceCriteria: (0, pg_core_1.jsonb)('acceptance_criteria').notNull().default('[]'),
|
|
51
|
+
constraints: (0, pg_core_1.jsonb)('constraints').notNull().default('[]'),
|
|
52
|
+
priority: (0, pg_core_1.smallint)('priority').notNull().default(3),
|
|
53
|
+
state: (0, exports.taskStateEnum)('state').notNull().default('draft'),
|
|
54
|
+
baseCommitSha: (0, pg_core_1.text)('base_commit_sha'),
|
|
55
|
+
activeExecutionLeaseId: (0, pg_core_1.uuid)('active_execution_lease_id'),
|
|
56
|
+
activeReviewLeaseId: (0, pg_core_1.uuid)('active_review_lease_id'),
|
|
57
|
+
nextFencingToken: (0, pg_core_1.bigint)('next_fencing_token', { mode: 'number' }).notNull().default(0),
|
|
58
|
+
version: (0, pg_core_1.bigint)('version', { mode: 'number' }).notNull().default(1),
|
|
59
|
+
createdBy: (0, pg_core_1.uuid)('created_by').notNull(),
|
|
60
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
61
|
+
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
62
|
+
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
63
|
+
});
|
|
64
|
+
exports.taskAttempts = (0, pg_core_1.pgTable)('task_attempts', {
|
|
65
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
66
|
+
workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull(),
|
|
67
|
+
taskId: (0, pg_core_1.uuid)('task_id').notNull().references(() => exports.tasks.id),
|
|
68
|
+
attemptNumber: (0, pg_core_1.integer)('attempt_number').notNull(),
|
|
69
|
+
agentInstanceId: (0, pg_core_1.uuid)('agent_instance_id').notNull(),
|
|
70
|
+
status: (0, pg_core_1.text)('status').notNull(),
|
|
71
|
+
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
72
|
+
endedAt: (0, pg_core_1.timestamp)('ended_at', { withTimezone: true }),
|
|
73
|
+
outcome: (0, pg_core_1.text)('outcome'),
|
|
74
|
+
}, (table) => ({
|
|
75
|
+
unq: (0, pg_core_1.unique)().on(table.taskId, table.attemptNumber)
|
|
76
|
+
}));
|
|
77
|
+
exports.leases = (0, pg_core_1.pgTable)('leases', {
|
|
78
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
79
|
+
workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull(),
|
|
80
|
+
taskId: (0, pg_core_1.uuid)('task_id').notNull().references(() => exports.tasks.id),
|
|
81
|
+
taskAttemptId: (0, pg_core_1.uuid)('task_attempt_id').references(() => exports.taskAttempts.id),
|
|
82
|
+
agentInstanceId: (0, pg_core_1.uuid)('agent_instance_id').notNull(),
|
|
83
|
+
leaseType: (0, exports.leaseTypeEnum)('lease_type').notNull(),
|
|
84
|
+
status: (0, exports.leaseStatusEnum)('status').notNull(),
|
|
85
|
+
fencingToken: (0, pg_core_1.bigint)('fencing_token', { mode: 'number' }).notNull(),
|
|
86
|
+
acquiredAt: (0, pg_core_1.timestamp)('acquired_at', { withTimezone: true }).notNull(),
|
|
87
|
+
renewedAt: (0, pg_core_1.timestamp)('renewed_at', { withTimezone: true }).notNull(),
|
|
88
|
+
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
89
|
+
releasedAt: (0, pg_core_1.timestamp)('released_at', { withTimezone: true }),
|
|
90
|
+
}, (table) => ({
|
|
91
|
+
unq: (0, pg_core_1.unique)().on(table.taskId, table.leaseType, table.fencingToken),
|
|
92
|
+
// Note: Drizzle partial unique index would be handled in migrations or sql
|
|
93
|
+
}));
|
|
94
|
+
exports.events = (0, pg_core_1.pgTable)('events', {
|
|
95
|
+
eventSeq: (0, pg_core_1.bigserial)('event_seq', { mode: 'number' }).primaryKey(),
|
|
96
|
+
eventId: (0, pg_core_1.uuid)('event_id').notNull().unique(),
|
|
97
|
+
eventType: (0, pg_core_1.text)('event_type').notNull(),
|
|
98
|
+
eventVersion: (0, pg_core_1.integer)('event_version').notNull(),
|
|
99
|
+
workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull(),
|
|
100
|
+
aggregateType: (0, pg_core_1.text)('aggregate_type').notNull(),
|
|
101
|
+
aggregateId: (0, pg_core_1.uuid)('aggregate_id').notNull(),
|
|
102
|
+
aggregateVersion: (0, pg_core_1.bigint)('aggregate_version', { mode: 'number' }).notNull(),
|
|
103
|
+
actorType: (0, pg_core_1.text)('actor_type').notNull(),
|
|
104
|
+
actorId: (0, pg_core_1.uuid)('actor_id'),
|
|
105
|
+
correlationId: (0, pg_core_1.uuid)('correlation_id').notNull(),
|
|
106
|
+
causationId: (0, pg_core_1.uuid)('causation_id'),
|
|
107
|
+
payload: (0, pg_core_1.jsonb)('payload').notNull(),
|
|
108
|
+
occurredAt: (0, pg_core_1.timestamp)('occurred_at', { withTimezone: true }).notNull().defaultNow(),
|
|
109
|
+
}, (table) => ({
|
|
110
|
+
unq: (0, pg_core_1.unique)().on(table.aggregateType, table.aggregateId, table.aggregateVersion),
|
|
111
|
+
idx: (0, pg_core_1.index)('events_workspace_cursor').on(table.workspaceId, table.eventSeq)
|
|
112
|
+
}));
|
|
113
|
+
exports.outbox = (0, pg_core_1.pgTable)('outbox', {
|
|
114
|
+
eventId: (0, pg_core_1.uuid)('event_id').primaryKey().references(() => exports.events.eventId, { onDelete: 'cascade' }),
|
|
115
|
+
availableAt: (0, pg_core_1.timestamp)('available_at', { withTimezone: true }).notNull().defaultNow(),
|
|
116
|
+
attempts: (0, pg_core_1.integer)('attempts').notNull().default(0),
|
|
117
|
+
lockedBy: (0, pg_core_1.text)('locked_by'),
|
|
118
|
+
lockedUntil: (0, pg_core_1.timestamp)('locked_until', { withTimezone: true }),
|
|
119
|
+
publishedAt: (0, pg_core_1.timestamp)('published_at', { withTimezone: true }),
|
|
120
|
+
lastError: (0, pg_core_1.text)('last_error'),
|
|
121
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
122
|
+
});
|
|
123
|
+
exports.idempotencyRecords = (0, pg_core_1.pgTable)('idempotency_records', {
|
|
124
|
+
principalId: (0, pg_core_1.uuid)('principal_id').notNull(),
|
|
125
|
+
operation: (0, pg_core_1.text)('operation').notNull(),
|
|
126
|
+
idempotencyKey: (0, pg_core_1.text)('idempotency_key').notNull(),
|
|
127
|
+
requestHash: (0, pg_core_1.text)('request_hash').notNull(),
|
|
128
|
+
responseJson: (0, pg_core_1.jsonb)('response_json'),
|
|
129
|
+
responseStatus: (0, pg_core_1.text)('response_status').notNull(),
|
|
130
|
+
resourceId: (0, pg_core_1.uuid)('resource_id'),
|
|
131
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
132
|
+
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
133
|
+
}, (table) => ({
|
|
134
|
+
pk: (0, pg_core_1.primaryKey)({ columns: [table.principalId, table.operation, table.idempotencyKey] }),
|
|
135
|
+
idx: (0, pg_core_1.index)('idempotency_expiry').on(table.expiresAt)
|
|
136
|
+
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.redis = void 0;
|
|
7
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
|
+
const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
|
|
9
|
+
exports.redis = new ioredis_1.default(redisUrl, {
|
|
10
|
+
maxRetriesPerRequest: null
|
|
11
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.eventService = exports.EventService = void 0;
|
|
4
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
class EventService {
|
|
7
|
+
/**
|
|
8
|
+
* Appends an event to the events table and inserts a corresponding outbox record
|
|
9
|
+
* as part of a transaction.
|
|
10
|
+
*/
|
|
11
|
+
async appendEvent(tx, params) {
|
|
12
|
+
const eventId = (0, crypto_1.randomUUID)();
|
|
13
|
+
// Insert into Event Store
|
|
14
|
+
await tx.insert(schema_js_1.events).values({
|
|
15
|
+
eventId,
|
|
16
|
+
eventType: params.eventType,
|
|
17
|
+
eventVersion: 1,
|
|
18
|
+
workspaceId: params.workspaceId,
|
|
19
|
+
aggregateType: params.aggregateType,
|
|
20
|
+
aggregateId: params.aggregateId,
|
|
21
|
+
aggregateVersion: params.aggregateVersion,
|
|
22
|
+
actorType: params.actorType,
|
|
23
|
+
actorId: params.actorId || null,
|
|
24
|
+
correlationId: eventId, // Base correlation id
|
|
25
|
+
payload: params.payload,
|
|
26
|
+
});
|
|
27
|
+
// Insert into Outbox
|
|
28
|
+
await tx.insert(schema_js_1.outbox).values({
|
|
29
|
+
eventId,
|
|
30
|
+
attempts: 0
|
|
31
|
+
});
|
|
32
|
+
return eventId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.EventService = EventService;
|
|
36
|
+
exports.eventService = new EventService();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentService = exports.AgentService = void 0;
|
|
4
|
+
const index_js_1 = require("../../infrastructure/db/index.js");
|
|
5
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
8
|
+
class AgentService {
|
|
9
|
+
async ensureDefaultWorkspace() {
|
|
10
|
+
let [workspace] = await index_js_1.db.select().from(schema_js_1.workspaces).limit(1);
|
|
11
|
+
if (!workspace) {
|
|
12
|
+
[workspace] = await index_js_1.db.insert(schema_js_1.workspaces).values({
|
|
13
|
+
id: (0, crypto_1.randomUUID)(),
|
|
14
|
+
name: "Default Workspace",
|
|
15
|
+
}).returning();
|
|
16
|
+
}
|
|
17
|
+
return workspace;
|
|
18
|
+
}
|
|
19
|
+
async registerAgent(params) {
|
|
20
|
+
const workspace = await this.ensureDefaultWorkspace();
|
|
21
|
+
// 1. Create or find agent definition
|
|
22
|
+
let [definition] = await index_js_1.db.select().from(schema_js_1.agentDefinitions)
|
|
23
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.agentDefinitions.name, params.name))
|
|
24
|
+
.limit(1);
|
|
25
|
+
if (!definition) {
|
|
26
|
+
[definition] = await index_js_1.db.insert(schema_js_1.agentDefinitions).values({
|
|
27
|
+
id: (0, crypto_1.randomUUID)(),
|
|
28
|
+
name: params.name,
|
|
29
|
+
vendor: params.vendor || "unknown",
|
|
30
|
+
version: params.version,
|
|
31
|
+
integrationType: "mcp-client"
|
|
32
|
+
}).returning();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Update version if changed
|
|
36
|
+
if (definition.version !== params.version) {
|
|
37
|
+
[definition] = await index_js_1.db.update(schema_js_1.agentDefinitions)
|
|
38
|
+
.set({ version: params.version, updatedAt: new Date() })
|
|
39
|
+
.where((0, drizzle_orm_1.eq)(schema_js_1.agentDefinitions.id, definition.id))
|
|
40
|
+
.returning();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 2. Create agent instance
|
|
44
|
+
const [instance] = await index_js_1.db.insert(schema_js_1.agentInstances).values({
|
|
45
|
+
id: (0, crypto_1.randomUUID)(),
|
|
46
|
+
agentDefinitionId: definition.id,
|
|
47
|
+
workspaceId: workspace.id,
|
|
48
|
+
principalId: (0, crypto_1.randomUUID)(), // Mocking principal ID for Phase 0
|
|
49
|
+
capabilitiesJson: params.capabilities,
|
|
50
|
+
status: "connected",
|
|
51
|
+
}).returning();
|
|
52
|
+
return instance;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.AgentService = AgentService;
|
|
56
|
+
exports.agentService = new AgentService();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.leaseService = exports.LeaseService = void 0;
|
|
4
|
+
const index_js_1 = require("../../infrastructure/db/index.js");
|
|
5
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
8
|
+
const event_service_js_1 = require("../event/event.service.js");
|
|
9
|
+
class LeaseService {
|
|
10
|
+
async acquireLease(params) {
|
|
11
|
+
const ttlMs = params.ttlMs || 30 * 60 * 1000; // default 30 mins
|
|
12
|
+
// We use a transaction to guarantee atomic lease acquisition (fencing token increment)
|
|
13
|
+
return await index_js_1.db.transaction(async (tx) => {
|
|
14
|
+
// 1. Lock the task row for update
|
|
15
|
+
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');
|
|
16
|
+
if (!task)
|
|
17
|
+
throw new Error("Task not found");
|
|
18
|
+
// Check if lease is already active
|
|
19
|
+
const activeLeaseId = params.leaseType === 'execution' ? task.activeExecutionLeaseId : task.activeReviewLeaseId;
|
|
20
|
+
if (activeLeaseId) {
|
|
21
|
+
const [activeLease] = await tx.select().from(schema_js_1.leases).where((0, drizzle_orm_1.eq)(schema_js_1.leases.id, activeLeaseId));
|
|
22
|
+
if (activeLease && activeLease.status === 'active' && new Date() < activeLease.expiresAt) {
|
|
23
|
+
throw new Error("Task is already actively leased");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 2. Increment fencing token
|
|
27
|
+
const nextFencingToken = task.nextFencingToken + 1;
|
|
28
|
+
// 3. Create the lease
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
31
|
+
const [lease] = await tx.insert(schema_js_1.leases).values({
|
|
32
|
+
id: (0, crypto_1.randomUUID)(),
|
|
33
|
+
workspaceId: params.workspaceId,
|
|
34
|
+
taskId: params.taskId,
|
|
35
|
+
agentInstanceId: params.agentInstanceId,
|
|
36
|
+
leaseType: params.leaseType,
|
|
37
|
+
status: 'active',
|
|
38
|
+
fencingToken: nextFencingToken,
|
|
39
|
+
acquiredAt: now,
|
|
40
|
+
renewedAt: now,
|
|
41
|
+
expiresAt: expiresAt,
|
|
42
|
+
}).returning();
|
|
43
|
+
// 4. Update task with active lease ID and incremented token, and set state to 'leased'
|
|
44
|
+
const updateData = {
|
|
45
|
+
nextFencingToken: nextFencingToken,
|
|
46
|
+
version: task.version + 1,
|
|
47
|
+
state: 'leased'
|
|
48
|
+
};
|
|
49
|
+
if (params.leaseType === 'execution') {
|
|
50
|
+
updateData.activeExecutionLeaseId = lease.id;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
updateData.activeReviewLeaseId = lease.id;
|
|
54
|
+
}
|
|
55
|
+
await tx.update(schema_js_1.tasks).set(updateData).where((0, drizzle_orm_1.eq)(schema_js_1.tasks.id, params.taskId));
|
|
56
|
+
await event_service_js_1.eventService.appendEvent(tx, {
|
|
57
|
+
workspaceId: params.workspaceId,
|
|
58
|
+
eventType: 'LeaseAcquired',
|
|
59
|
+
aggregateType: 'Task',
|
|
60
|
+
aggregateId: task.id,
|
|
61
|
+
aggregateVersion: updateData.version,
|
|
62
|
+
actorType: 'AgentInstance',
|
|
63
|
+
actorId: params.agentInstanceId,
|
|
64
|
+
payload: {
|
|
65
|
+
leaseId: lease.id,
|
|
66
|
+
leaseType: params.leaseType,
|
|
67
|
+
fencingToken: nextFencingToken,
|
|
68
|
+
expiresAt: expiresAt.toISOString()
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return lease;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.LeaseService = LeaseService;
|
|
76
|
+
exports.leaseService = new LeaseService();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taskService = exports.TaskService = void 0;
|
|
4
|
+
const index_js_1 = require("../../infrastructure/db/index.js");
|
|
5
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const event_service_js_1 = require("../event/event.service.js");
|
|
8
|
+
class TaskService {
|
|
9
|
+
async createTask(params) {
|
|
10
|
+
return await index_js_1.db.transaction(async (tx) => {
|
|
11
|
+
const taskId = (0, crypto_1.randomUUID)();
|
|
12
|
+
const [task] = await tx.insert(schema_js_1.tasks).values({
|
|
13
|
+
id: taskId,
|
|
14
|
+
workspaceId: params.workspaceId,
|
|
15
|
+
repositoryId: params.repositoryId,
|
|
16
|
+
objective: params.objective,
|
|
17
|
+
description: params.description || '',
|
|
18
|
+
createdBy: params.createdBy,
|
|
19
|
+
state: 'ready',
|
|
20
|
+
}).returning();
|
|
21
|
+
await event_service_js_1.eventService.appendEvent(tx, {
|
|
22
|
+
workspaceId: params.workspaceId,
|
|
23
|
+
eventType: 'TaskCreated',
|
|
24
|
+
aggregateType: 'Task',
|
|
25
|
+
aggregateId: task.id,
|
|
26
|
+
aggregateVersion: task.version,
|
|
27
|
+
actorType: 'User',
|
|
28
|
+
actorId: params.createdBy,
|
|
29
|
+
payload: {
|
|
30
|
+
objective: task.objective,
|
|
31
|
+
repositoryId: task.repositoryId,
|
|
32
|
+
state: task.state
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return task;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.TaskService = TaskService;
|
|
40
|
+
exports.taskService = new TaskService();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupMcpTransport = setupMcpTransport;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const agent_service_js_1 = require("../../modules/identity/agent.service.js");
|
|
8
|
+
const task_service_js_1 = require("../../modules/task/task.service.js");
|
|
9
|
+
const lease_service_js_1 = require("../../modules/lease/lease.service.js");
|
|
10
|
+
async function setupMcpTransport(fastify) {
|
|
11
|
+
// Store transports for the POST /mcp/message route
|
|
12
|
+
const transports = new Map();
|
|
13
|
+
fastify.get("/mcp/sse", async (request, reply) => {
|
|
14
|
+
// Hijack the Fastify response so we can stream SSE headers manually
|
|
15
|
+
reply.hijack();
|
|
16
|
+
const transport = new sse_js_1.SSEServerTransport("/mcp/message", reply.raw);
|
|
17
|
+
const sessionId = transport.sessionId;
|
|
18
|
+
console.log(`New SSE connection: ${sessionId}`);
|
|
19
|
+
transports.set(sessionId, transport);
|
|
20
|
+
// Create a new MCP Server instance per connection
|
|
21
|
+
const mcpServer = new index_js_1.Server({ name: "mcp-agent-broker", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
22
|
+
// Setup tools
|
|
23
|
+
mcpServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
24
|
+
return {
|
|
25
|
+
tools: [
|
|
26
|
+
{
|
|
27
|
+
name: "register_agent",
|
|
28
|
+
description: "Register a new coding agent instance",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
name: { type: "string" },
|
|
33
|
+
version: { type: "string" },
|
|
34
|
+
capabilities: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: { type: "string" }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ["name", "version", "capabilities"]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "create_task",
|
|
44
|
+
description: "Create a new coding task",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
workspaceId: { type: "string" },
|
|
49
|
+
repositoryId: { type: "string" },
|
|
50
|
+
objective: { type: "string" },
|
|
51
|
+
description: { type: "string" },
|
|
52
|
+
createdBy: { type: "string" }
|
|
53
|
+
},
|
|
54
|
+
required: ["workspaceId", "repositoryId", "objective", "createdBy"]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "acquire_task_lease",
|
|
59
|
+
description: "Acquire an atomic lease for a task (execution or review)",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
taskId: { type: "string" },
|
|
64
|
+
workspaceId: { type: "string" },
|
|
65
|
+
agentInstanceId: { type: "string" },
|
|
66
|
+
leaseType: { type: "string", enum: ["execution", "review"] }
|
|
67
|
+
},
|
|
68
|
+
required: ["taskId", "workspaceId", "agentInstanceId", "leaseType"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
mcpServer.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
75
|
+
if (req.params.name === "register_agent") {
|
|
76
|
+
try {
|
|
77
|
+
const args = req.params.arguments;
|
|
78
|
+
const instance = await agent_service_js_1.agentService.registerAgent({
|
|
79
|
+
name: args.name,
|
|
80
|
+
version: args.version,
|
|
81
|
+
capabilities: args.capabilities,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Agent registered successfully! Instance ID: ${instance.id}`
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error("Error in register_agent:", e);
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (req.params.name === "create_task") {
|
|
98
|
+
try {
|
|
99
|
+
const args = req.params.arguments;
|
|
100
|
+
const task = await task_service_js_1.taskService.createTask({
|
|
101
|
+
workspaceId: args.workspaceId,
|
|
102
|
+
repositoryId: args.repositoryId,
|
|
103
|
+
objective: args.objective,
|
|
104
|
+
description: args.description,
|
|
105
|
+
createdBy: args.createdBy
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{ type: "text", text: `Task created successfully! Task ID: ${task.id}` }
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.error("Error in create_task:", e);
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (req.params.name === "acquire_task_lease") {
|
|
119
|
+
try {
|
|
120
|
+
const args = req.params.arguments;
|
|
121
|
+
const lease = await lease_service_js_1.leaseService.acquireLease({
|
|
122
|
+
taskId: args.taskId,
|
|
123
|
+
workspaceId: args.workspaceId,
|
|
124
|
+
agentInstanceId: args.agentInstanceId,
|
|
125
|
+
leaseType: args.leaseType
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{ type: "text", text: `Lease acquired! Lease ID: ${lease.id}, Fencing Token: ${lease.fencingToken}` }
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
console.error("Error in acquire_task_lease:", e);
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Tool not found: ${req.params.name}`);
|
|
139
|
+
});
|
|
140
|
+
await mcpServer.connect(transport);
|
|
141
|
+
request.raw.on("close", () => {
|
|
142
|
+
console.log(`SSE connection closed: ${sessionId}`);
|
|
143
|
+
transports.delete(sessionId);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
fastify.post("/mcp/message", async (request, reply) => {
|
|
147
|
+
const sessionId = request.query.sessionId;
|
|
148
|
+
const transport = transports.get(sessionId);
|
|
149
|
+
if (!transport) {
|
|
150
|
+
reply.code(404).send({ error: "Session not found" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Fastify parses the body automatically
|
|
154
|
+
await transport.handleMessage(request.body);
|
|
155
|
+
reply.code(202).send("Accepted");
|
|
156
|
+
});
|
|
157
|
+
}
|