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.
Files changed (32) hide show
  1. package/README.md +31 -0
  2. package/dist/app.js +30 -0
  3. package/dist/infrastructure/db/index.js +44 -0
  4. package/dist/infrastructure/db/schema.js +136 -0
  5. package/dist/infrastructure/redis/index.js +11 -0
  6. package/dist/modules/event/event.service.js +36 -0
  7. package/dist/modules/identity/agent.service.js +56 -0
  8. package/dist/modules/lease/lease.service.js +76 -0
  9. package/dist/modules/task/task.service.js +40 -0
  10. package/dist/transport/mcp/index.js +157 -0
  11. package/dist/workers/outbox-relay.js +57 -0
  12. package/docker-compose.yml +41 -0
  13. package/drizzle.config.ts +10 -0
  14. package/package.json +35 -0
  15. package/src/app.ts +31 -0
  16. package/src/infrastructure/db/index.ts +10 -0
  17. package/src/infrastructure/db/migrations/0000_flawless_vanisher.sql +103 -0
  18. package/src/infrastructure/db/migrations/0001_fearless_sersi.sql +36 -0
  19. package/src/infrastructure/db/migrations/meta/0000_snapshot.json +714 -0
  20. package/src/infrastructure/db/migrations/meta/0001_snapshot.json +947 -0
  21. package/src/infrastructure/db/migrations/meta/_journal.json +20 -0
  22. package/src/infrastructure/db/schema.ts +148 -0
  23. package/src/infrastructure/redis/index.ts +7 -0
  24. package/src/modules/event/event.service.ts +50 -0
  25. package/src/modules/identity/agent.service.ts +58 -0
  26. package/src/modules/lease/lease.service.ts +89 -0
  27. package/src/modules/task/task.service.ts +46 -0
  28. package/src/transport/mcp/index.ts +167 -0
  29. package/src/workers/outbox-relay.ts +61 -0
  30. package/test_client.ts +34 -0
  31. package/test_task.ts +68 -0
  32. 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
+ }