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
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.outboxRelay = exports.OutboxRelayWorker = void 0;
4
+ const index_js_1 = require("../infrastructure/db/index.js");
5
+ const schema_js_1 = require("../infrastructure/db/schema.js");
6
+ const index_js_2 = require("../infrastructure/redis/index.js");
7
+ const drizzle_orm_1 = require("drizzle-orm");
8
+ class OutboxRelayWorker {
9
+ timer = null;
10
+ isRunning = false;
11
+ start() {
12
+ console.log("Starting Outbox Relay Worker...");
13
+ this.timer = setInterval(() => this.processOutbox(), 2000);
14
+ }
15
+ stop() {
16
+ if (this.timer)
17
+ clearInterval(this.timer);
18
+ }
19
+ async processOutbox() {
20
+ if (this.isRunning)
21
+ return;
22
+ this.isRunning = true;
23
+ try {
24
+ // 1. Fetch pending outbox events
25
+ const pendingRecords = await index_js_1.db.select()
26
+ .from(schema_js_1.outbox)
27
+ .orderBy((0, drizzle_orm_1.asc)(schema_js_1.outbox.createdAt))
28
+ .limit(50);
29
+ if (pendingRecords.length === 0) {
30
+ this.isRunning = false;
31
+ return;
32
+ }
33
+ const eventIds = pendingRecords.map(r => r.eventId);
34
+ // 2. Fetch the actual event data
35
+ const eventData = await index_js_1.db.select().from(schema_js_1.events).where((0, drizzle_orm_1.inArray)(schema_js_1.events.eventId, eventIds));
36
+ // 3. Publish to Redis Pub/Sub
37
+ for (const record of pendingRecords) {
38
+ const evt = eventData.find(e => e.eventId === record.eventId);
39
+ if (evt) {
40
+ const channel = `workspace:${evt.workspaceId}:events`;
41
+ await index_js_2.redis.publish(channel, JSON.stringify(evt));
42
+ console.log(`[Event Relay] Published ${evt.eventType} to ${channel}`);
43
+ }
44
+ }
45
+ // 4. Delete processed outbox records
46
+ await index_js_1.db.delete(schema_js_1.outbox).where((0, drizzle_orm_1.inArray)(schema_js_1.outbox.eventId, eventIds));
47
+ }
48
+ catch (e) {
49
+ console.error("Outbox Relay Error:", e);
50
+ }
51
+ finally {
52
+ this.isRunning = false;
53
+ }
54
+ }
55
+ }
56
+ exports.OutboxRelayWorker = OutboxRelayWorker;
57
+ exports.outboxRelay = new OutboxRelayWorker();
@@ -0,0 +1,41 @@
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:
@@ -0,0 +1,10 @@
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/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "mcp-agent-broker",
3
+ "version": "1.0.0",
4
+ "main": "dist/app.js",
5
+ "bin": {
6
+ "mcp-agent-broker": "dist/app.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "start": "node dist/app.js",
11
+ "dev": "tsx watch src/app.ts",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "description": "",
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
20
+ "drizzle-orm": "^0.45.2",
21
+ "fastify": "^5.10.0",
22
+ "fastify-plugin": "^6.0.0",
23
+ "ioredis": "^5.11.1",
24
+ "pg": "^8.22.0",
25
+ "zod": "^4.4.3"
26
+ },
27
+ "devDependencies": {
28
+ "@types/ioredis": "^4.28.10",
29
+ "@types/node": "^26.1.1",
30
+ "@types/pg": "^8.20.0",
31
+ "drizzle-kit": "^0.31.10",
32
+ "tsx": "^4.23.1",
33
+ "typescript": "^7.0.2"
34
+ }
35
+ }
package/src/app.ts ADDED
@@ -0,0 +1,31 @@
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();
@@ -0,0 +1,10 @@
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 });
@@ -0,0 +1,103 @@
1
+ CREATE TYPE "public"."lease_status" AS ENUM('active', 'released', 'expired', 'revoked');--> statement-breakpoint
2
+ CREATE TYPE "public"."lease_type" AS ENUM('execution', 'review');--> statement-breakpoint
3
+ CREATE TYPE "public"."task_state" AS ENUM('draft', 'ready', 'leased', 'running', 'blocked', 'awaiting_review', 'changes_requested', 'completed', 'failed', 'canceled', 'superseded');--> statement-breakpoint
4
+ CREATE TABLE "events" (
5
+ "event_seq" bigserial PRIMARY KEY NOT NULL,
6
+ "event_id" uuid NOT NULL,
7
+ "event_type" text NOT NULL,
8
+ "event_version" integer NOT NULL,
9
+ "workspace_id" uuid NOT NULL,
10
+ "aggregate_type" text NOT NULL,
11
+ "aggregate_id" uuid NOT NULL,
12
+ "aggregate_version" bigint NOT NULL,
13
+ "actor_type" text NOT NULL,
14
+ "actor_id" uuid,
15
+ "correlation_id" uuid NOT NULL,
16
+ "causation_id" uuid,
17
+ "payload" jsonb NOT NULL,
18
+ "occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
19
+ CONSTRAINT "events_event_id_unique" UNIQUE("event_id"),
20
+ CONSTRAINT "events_aggregate_type_aggregate_id_aggregate_version_unique" UNIQUE("aggregate_type","aggregate_id","aggregate_version")
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE TABLE "idempotency_records" (
24
+ "principal_id" uuid NOT NULL,
25
+ "operation" text NOT NULL,
26
+ "idempotency_key" text NOT NULL,
27
+ "request_hash" text NOT NULL,
28
+ "response_json" jsonb,
29
+ "response_status" text NOT NULL,
30
+ "resource_id" uuid,
31
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
32
+ "expires_at" timestamp with time zone NOT NULL,
33
+ CONSTRAINT "idempotency_records_principal_id_operation_idempotency_key_pk" PRIMARY KEY("principal_id","operation","idempotency_key")
34
+ );
35
+ --> statement-breakpoint
36
+ CREATE TABLE "leases" (
37
+ "id" uuid PRIMARY KEY NOT NULL,
38
+ "workspace_id" uuid NOT NULL,
39
+ "task_id" uuid NOT NULL,
40
+ "task_attempt_id" uuid,
41
+ "agent_instance_id" uuid NOT NULL,
42
+ "lease_type" "lease_type" NOT NULL,
43
+ "status" "lease_status" NOT NULL,
44
+ "fencing_token" bigint NOT NULL,
45
+ "acquired_at" timestamp with time zone NOT NULL,
46
+ "renewed_at" timestamp with time zone NOT NULL,
47
+ "expires_at" timestamp with time zone NOT NULL,
48
+ "released_at" timestamp with time zone,
49
+ CONSTRAINT "leases_task_id_lease_type_fencing_token_unique" UNIQUE("task_id","lease_type","fencing_token")
50
+ );
51
+ --> statement-breakpoint
52
+ CREATE TABLE "outbox" (
53
+ "event_id" uuid PRIMARY KEY NOT NULL,
54
+ "available_at" timestamp with time zone DEFAULT now() NOT NULL,
55
+ "attempts" integer DEFAULT 0 NOT NULL,
56
+ "locked_by" text,
57
+ "locked_until" timestamp with time zone,
58
+ "published_at" timestamp with time zone,
59
+ "last_error" text,
60
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
61
+ );
62
+ --> statement-breakpoint
63
+ CREATE TABLE "task_attempts" (
64
+ "id" uuid PRIMARY KEY NOT NULL,
65
+ "workspace_id" uuid NOT NULL,
66
+ "task_id" uuid NOT NULL,
67
+ "attempt_number" integer NOT NULL,
68
+ "agent_instance_id" uuid NOT NULL,
69
+ "status" text NOT NULL,
70
+ "started_at" timestamp with time zone,
71
+ "ended_at" timestamp with time zone,
72
+ "outcome" text,
73
+ CONSTRAINT "task_attempts_task_id_attempt_number_unique" UNIQUE("task_id","attempt_number")
74
+ );
75
+ --> statement-breakpoint
76
+ CREATE TABLE "tasks" (
77
+ "id" uuid PRIMARY KEY NOT NULL,
78
+ "workspace_id" uuid NOT NULL,
79
+ "repository_id" uuid NOT NULL,
80
+ "session_id" uuid,
81
+ "objective" text NOT NULL,
82
+ "description" text DEFAULT '' NOT NULL,
83
+ "acceptance_criteria" jsonb DEFAULT '[]' NOT NULL,
84
+ "constraints" jsonb DEFAULT '[]' NOT NULL,
85
+ "priority" smallint DEFAULT 3 NOT NULL,
86
+ "state" "task_state" DEFAULT 'draft' NOT NULL,
87
+ "base_commit_sha" text,
88
+ "active_execution_lease_id" uuid,
89
+ "active_review_lease_id" uuid,
90
+ "next_fencing_token" bigint DEFAULT 0 NOT NULL,
91
+ "version" bigint DEFAULT 1 NOT NULL,
92
+ "created_by" uuid NOT NULL,
93
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
94
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
95
+ "completed_at" timestamp with time zone
96
+ );
97
+ --> statement-breakpoint
98
+ ALTER TABLE "leases" ADD CONSTRAINT "leases_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
99
+ ALTER TABLE "leases" ADD CONSTRAINT "leases_task_attempt_id_task_attempts_id_fk" FOREIGN KEY ("task_attempt_id") REFERENCES "public"."task_attempts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
100
+ ALTER TABLE "outbox" ADD CONSTRAINT "outbox_event_id_events_event_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."events"("event_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
101
+ ALTER TABLE "task_attempts" ADD CONSTRAINT "task_attempts_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
102
+ CREATE INDEX "events_workspace_cursor" ON "events" USING btree ("workspace_id","event_seq");--> statement-breakpoint
103
+ CREATE INDEX "idempotency_expiry" ON "idempotency_records" USING btree ("expires_at");
@@ -0,0 +1,36 @@
1
+ CREATE TYPE "public"."agent_status" AS ENUM('connected', 'degraded', 'disconnected', 'revoked');--> statement-breakpoint
2
+ CREATE TYPE "public"."workspace_status" AS ENUM('active', 'archived');--> statement-breakpoint
3
+ CREATE TABLE "agent_definitions" (
4
+ "id" uuid PRIMARY KEY NOT NULL,
5
+ "name" text NOT NULL,
6
+ "vendor" text,
7
+ "version" text NOT NULL,
8
+ "integration_type" text,
9
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
10
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE TABLE "agent_instances" (
14
+ "id" uuid PRIMARY KEY NOT NULL,
15
+ "agent_definition_id" uuid NOT NULL,
16
+ "workspace_id" uuid NOT NULL,
17
+ "principal_id" uuid NOT NULL,
18
+ "status" "agent_status" DEFAULT 'connected' NOT NULL,
19
+ "capabilities_json" jsonb DEFAULT '[]' NOT NULL,
20
+ "last_heartbeat_at" timestamp with time zone,
21
+ "revoked_at" timestamp with time zone,
22
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
23
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
24
+ );
25
+ --> statement-breakpoint
26
+ CREATE TABLE "workspaces" (
27
+ "id" uuid PRIMARY KEY NOT NULL,
28
+ "organization_id" uuid,
29
+ "name" text NOT NULL,
30
+ "status" "workspace_status" DEFAULT 'active' NOT NULL,
31
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
32
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
33
+ );
34
+ --> statement-breakpoint
35
+ ALTER TABLE "agent_instances" ADD CONSTRAINT "agent_instances_agent_definition_id_agent_definitions_id_fk" FOREIGN KEY ("agent_definition_id") REFERENCES "public"."agent_definitions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
36
+ ALTER TABLE "agent_instances" ADD CONSTRAINT "agent_instances_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE no action ON UPDATE no action;