@renseiai/agentfactory-mcp-server 0.8.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.
@@ -0,0 +1,6 @@
1
+ export declare function isMcpAuthConfigured(): boolean;
2
+ export declare function verifyMcpAuth(authHeader: string | null | undefined): {
3
+ authorized: boolean;
4
+ error?: string;
5
+ };
6
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAIA,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAuB5G"}
@@ -0,0 +1,25 @@
1
+ import { extractBearerToken, verifyApiKey, isWorkerAuthConfigured } from '@renseiai/agentfactory-server';
2
+ const MCP_API_KEY_ENV = 'MCP_API_KEY';
3
+ export function isMcpAuthConfigured() {
4
+ return isWorkerAuthConfigured(MCP_API_KEY_ENV) || isWorkerAuthConfigured('WORKER_API_KEY');
5
+ }
6
+ export function verifyMcpAuth(authHeader) {
7
+ if (!isMcpAuthConfigured()) {
8
+ // No auth configured — allow all requests (dev mode)
9
+ return { authorized: true };
10
+ }
11
+ const token = extractBearerToken(authHeader);
12
+ if (!token) {
13
+ return { authorized: false, error: 'Missing or invalid Authorization header. Expected: Bearer <api-key>' };
14
+ }
15
+ // Try MCP-specific key first, then fall back to worker key
16
+ const mcpKey = process.env[MCP_API_KEY_ENV];
17
+ const workerKey = process.env.WORKER_API_KEY;
18
+ if (mcpKey && verifyApiKey(token, mcpKey)) {
19
+ return { authorized: true };
20
+ }
21
+ if (workerKey && verifyApiKey(token, workerKey)) {
22
+ return { authorized: true };
23
+ }
24
+ return { authorized: false, error: 'Invalid API key' };
25
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { createFleetMcpServer } from './server.js';
3
+ import { startStdioTransport, startHttpTransport } from './transport.js';
4
+ import { stopResourceNotificationPoller } from './resources.js';
5
+ const args = process.argv.slice(2);
6
+ const transportType = args.includes('--stdio') ? 'stdio' : 'http';
7
+ const server = createFleetMcpServer();
8
+ if (transportType === 'stdio') {
9
+ console.error('[mcp-server] Starting in STDIO mode');
10
+ await startStdioTransport(server);
11
+ // Graceful shutdown for STDIO mode
12
+ const shutdown = () => {
13
+ console.error('[mcp-server] Shutting down...');
14
+ stopResourceNotificationPoller();
15
+ process.exit(0);
16
+ };
17
+ process.on('SIGINT', shutdown);
18
+ process.on('SIGTERM', shutdown);
19
+ }
20
+ else {
21
+ const portIdx = args.indexOf('--port');
22
+ const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : undefined;
23
+ const hostIdx = args.indexOf('--host');
24
+ const host = hostIdx !== -1 ? args[hostIdx + 1] : undefined;
25
+ const httpServer = await startHttpTransport(server, { port, host });
26
+ // Graceful shutdown for HTTP mode — close server before exiting
27
+ const shutdown = () => {
28
+ console.log('[mcp-server] Shutting down...');
29
+ stopResourceNotificationPoller();
30
+ httpServer.close(() => {
31
+ process.exit(0);
32
+ });
33
+ // Force exit after 5s if connections don't drain
34
+ setTimeout(() => process.exit(0), 5000).unref();
35
+ };
36
+ process.on('SIGINT', shutdown);
37
+ process.on('SIGTERM', shutdown);
38
+ }
@@ -0,0 +1,6 @@
1
+ export { createFleetMcpServer } from './server.js';
2
+ export { registerFleetTools } from './tools.js';
3
+ export { registerFleetResources, stopResourceNotificationPoller } from './resources.js';
4
+ export { startStdioTransport, startHttpTransport, type HttpTransportOptions } from './transport.js';
5
+ export { verifyMcpAuth, isMcpAuthConfigured } from './auth.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAA;AACvF,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AACnG,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { createFleetMcpServer } from './server.js';
2
+ export { registerFleetTools } from './tools.js';
3
+ export { registerFleetResources, stopResourceNotificationPoller } from './resources.js';
4
+ export { startStdioTransport, startHttpTransport } from './transport.js';
5
+ export { verifyMcpAuth, isMcpAuthConfigured } from './auth.js';
@@ -0,0 +1,16 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Registers MCP resources that expose AgentFactory fleet state to MCP-aware clients.
4
+ *
5
+ * Resources:
6
+ * - fleet://agents — Current fleet state (agents, statuses, costs)
7
+ * - fleet://issues/{id} — Issue details with agent progress
8
+ * - fleet://logs/{id} — Agent activity logs / session info
9
+ *
10
+ * Also starts a polling loop that emits resource-update notifications
11
+ * when fleet state changes, enabling MCP clients to subscribe to updates.
12
+ */
13
+ export declare function registerFleetResources(server: McpServer): void;
14
+ /** Stop the resource notification poller (for graceful shutdown) */
15
+ export declare function stopResourceNotificationPoller(): void;
16
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAUxE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsG9D;AA4DD,oEAAoE;AACpE,wBAAgB,8BAA8B,IAAI,IAAI,CAKrD"}
@@ -0,0 +1,142 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { getAllSessions, getSessionState, getSessionStateByIssue, listWorkers, getTotalCapacity, } from '@renseiai/agentfactory-server';
3
+ /**
4
+ * Registers MCP resources that expose AgentFactory fleet state to MCP-aware clients.
5
+ *
6
+ * Resources:
7
+ * - fleet://agents — Current fleet state (agents, statuses, costs)
8
+ * - fleet://issues/{id} — Issue details with agent progress
9
+ * - fleet://logs/{id} — Agent activity logs / session info
10
+ *
11
+ * Also starts a polling loop that emits resource-update notifications
12
+ * when fleet state changes, enabling MCP clients to subscribe to updates.
13
+ */
14
+ export function registerFleetResources(server) {
15
+ // 1. fleet://agents — Current fleet state
16
+ server.resource('fleet-agents', 'fleet://agents', { description: 'Current fleet state including all agent sessions, workers, and capacity' }, async (uri) => {
17
+ const [sessions, workers, capacity] = await Promise.all([
18
+ getAllSessions(),
19
+ listWorkers(),
20
+ getTotalCapacity(),
21
+ ]);
22
+ const data = { sessions, workers, capacity };
23
+ return {
24
+ contents: [
25
+ {
26
+ uri: uri.href,
27
+ text: JSON.stringify(data, null, 2),
28
+ mimeType: 'application/json',
29
+ },
30
+ ],
31
+ };
32
+ });
33
+ // 2. fleet://issues/{id} — Issue details with agent progress
34
+ server.resource('fleet-issue', new ResourceTemplate('fleet://issues/{id}', { list: undefined }), { description: 'Issue details with agent session progress' }, async (uri, variables) => {
35
+ const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
36
+ const session = await getSessionStateByIssue(id);
37
+ const data = session
38
+ ? session
39
+ : { error: 'not_found', message: `No agent session found for issue: ${id}` };
40
+ return {
41
+ contents: [
42
+ {
43
+ uri: uri.href,
44
+ text: JSON.stringify(data, null, 2),
45
+ mimeType: 'application/json',
46
+ },
47
+ ],
48
+ };
49
+ });
50
+ // 3. fleet://logs/{id} — Agent activity logs
51
+ server.resource('fleet-logs', new ResourceTemplate('fleet://logs/{id}', { list: undefined }), { description: 'Agent activity logs and session information' }, async (uri, variables) => {
52
+ const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
53
+ // Try to find the session by linearSessionId first, then fall back to issue ID
54
+ let session = await getSessionState(id);
55
+ if (!session) {
56
+ session = await getSessionStateByIssue(id);
57
+ }
58
+ if (!session) {
59
+ const data = { error: 'not_found', message: `No agent session found for id: ${id}` };
60
+ return {
61
+ contents: [
62
+ {
63
+ uri: uri.href,
64
+ text: JSON.stringify(data, null, 2),
65
+ mimeType: 'application/json',
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ const data = {
71
+ ...session,
72
+ _logHint: session.worktreePath
73
+ ? `Activity logs may be available on disk at: ${session.worktreePath}/.agent/state.json`
74
+ : 'No worktree path available for this session',
75
+ };
76
+ return {
77
+ contents: [
78
+ {
79
+ uri: uri.href,
80
+ text: JSON.stringify(data, null, 2),
81
+ mimeType: 'application/json',
82
+ },
83
+ ],
84
+ };
85
+ });
86
+ // ─── Resource update notifications ────────────────────────────────────
87
+ // Poll fleet state and emit MCP resource-update notifications when changes
88
+ // are detected, so subscribed clients automatically refresh.
89
+ startResourceNotificationPoller(server);
90
+ }
91
+ // ─── Polling-based resource notifications ─────────────────────────────────
92
+ const POLL_INTERVAL_MS = 5_000;
93
+ let pollTimer = null;
94
+ function buildSnapshot(sessions) {
95
+ const counts = new Map();
96
+ for (const s of sessions) {
97
+ counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
98
+ }
99
+ const statusSummary = [...counts.entries()]
100
+ .sort(([a], [b]) => a.localeCompare(b))
101
+ .map(([k, v]) => `${k}:${v}`)
102
+ .join(',');
103
+ return { sessionCount: sessions.length, statusSummary };
104
+ }
105
+ function snapshotsEqual(a, b) {
106
+ if (!a)
107
+ return false;
108
+ return a.sessionCount === b.sessionCount && a.statusSummary === b.statusSummary;
109
+ }
110
+ function startResourceNotificationPoller(server) {
111
+ let lastSnapshot = null;
112
+ pollTimer = setInterval(async () => {
113
+ try {
114
+ const sessions = await getAllSessions();
115
+ const snapshot = buildSnapshot(sessions);
116
+ if (!snapshotsEqual(lastSnapshot, snapshot)) {
117
+ lastSnapshot = snapshot;
118
+ // Notify subscribed clients that fleet://agents has changed
119
+ try {
120
+ await server.server.sendResourceUpdated({ uri: 'fleet://agents' });
121
+ }
122
+ catch {
123
+ // Client may not be subscribed — safe to ignore
124
+ }
125
+ }
126
+ }
127
+ catch {
128
+ // Redis may be unavailable — skip this tick
129
+ }
130
+ }, POLL_INTERVAL_MS);
131
+ // Ensure the timer doesn't prevent process exit
132
+ if (pollTimer && typeof pollTimer === 'object' && 'unref' in pollTimer) {
133
+ pollTimer.unref();
134
+ }
135
+ }
136
+ /** Stop the resource notification poller (for graceful shutdown) */
137
+ export function stopResourceNotificationPoller() {
138
+ if (pollTimer) {
139
+ clearInterval(pollTimer);
140
+ pollTimer = null;
141
+ }
142
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createFleetMcpServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAInE,wBAAgB,oBAAoB,IAAI,SAAS,CAUhD"}
@@ -0,0 +1,12 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerFleetResources } from './resources.js';
3
+ import { registerFleetTools } from './tools.js';
4
+ export function createFleetMcpServer() {
5
+ const server = new McpServer({
6
+ name: 'agentfactory-fleet',
7
+ version: '0.7.52',
8
+ });
9
+ registerFleetTools(server);
10
+ registerFleetResources(server);
11
+ return server;
12
+ }
@@ -0,0 +1,14 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Register all fleet management tools on the MCP server.
4
+ *
5
+ * Tools registered:
6
+ * - submit-task: Submit a development task to the fleet work queue
7
+ * - get-task-status: Get current status of a task by session or issue ID
8
+ * - list-fleet: List agents and their statuses with optional filtering
9
+ * - get-cost-report: Get cost/token usage for a task or the entire fleet
10
+ * - stop-agent: Request to stop a running agent
11
+ * - forward-prompt: Forward a follow-up prompt to a running agent session
12
+ */
13
+ export declare function registerFleetTools(server: McpServer): void;
14
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAqCxE;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqX1D"}
@@ -0,0 +1,333 @@
1
+ import { z } from 'zod';
2
+ import { getAllSessions, getSessionsByStatus, getSessionState, getSessionStateByIssue, storeSessionState, updateSessionStatus, storePendingPrompt, } from '@renseiai/agentfactory-server';
3
+ // Work type values for zod enum validation
4
+ const WORK_TYPES = [
5
+ 'research',
6
+ 'backlog-creation',
7
+ 'development',
8
+ 'inflight',
9
+ 'qa',
10
+ 'acceptance',
11
+ 'refinement',
12
+ 'coordination',
13
+ 'qa-coordination',
14
+ 'acceptance-coordination',
15
+ ];
16
+ // Session status values for zod enum validation
17
+ const SESSION_STATUSES = [
18
+ 'pending',
19
+ 'claimed',
20
+ 'running',
21
+ 'finalizing',
22
+ 'completed',
23
+ 'failed',
24
+ 'stopped',
25
+ ];
26
+ /**
27
+ * Register all fleet management tools on the MCP server.
28
+ *
29
+ * Tools registered:
30
+ * - submit-task: Submit a development task to the fleet work queue
31
+ * - get-task-status: Get current status of a task by session or issue ID
32
+ * - list-fleet: List agents and their statuses with optional filtering
33
+ * - get-cost-report: Get cost/token usage for a task or the entire fleet
34
+ * - stop-agent: Request to stop a running agent
35
+ * - forward-prompt: Forward a follow-up prompt to a running agent session
36
+ */
37
+ export function registerFleetTools(server) {
38
+ // ─── submit-task ───────────────────────────────────────────────────
39
+ server.tool('submit-task', 'Submit a development task to the fleet work queue. Creates a pending session that a worker will pick up.', {
40
+ issueId: z.string().describe('Linear issue ID to work on'),
41
+ description: z.string().optional().describe('Optional description or prompt context for the task'),
42
+ workType: z.enum(WORK_TYPES).optional().describe('Type of work to perform (defaults to development)'),
43
+ priority: z.number().min(1).max(5).optional().describe('Priority 1-5 where 1 is highest (defaults to 3)'),
44
+ }, async (args) => {
45
+ try {
46
+ const linearSessionId = `mcp-${Date.now()}-${args.issueId}`;
47
+ const session = await storeSessionState(linearSessionId, {
48
+ issueId: args.issueId,
49
+ providerSessionId: null,
50
+ worktreePath: '',
51
+ status: 'pending',
52
+ priority: args.priority ?? 3,
53
+ promptContext: args.description,
54
+ workType: args.workType ?? 'development',
55
+ queuedAt: Math.floor(Date.now() / 1000),
56
+ });
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: JSON.stringify({
62
+ submitted: true,
63
+ taskId: session.linearSessionId,
64
+ issueId: session.issueId,
65
+ status: session.status,
66
+ priority: session.priority,
67
+ workType: session.workType,
68
+ }, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ }
73
+ catch (error) {
74
+ return {
75
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
76
+ isError: true,
77
+ };
78
+ }
79
+ });
80
+ // ─── get-task-status ───────────────────────────────────────────────
81
+ server.tool('get-task-status', 'Get the current status of a task. Accepts either a session ID (taskId) or a Linear issue ID.', {
82
+ taskId: z.string().describe('Session ID or Linear issue ID to look up'),
83
+ }, async (args) => {
84
+ try {
85
+ // Try direct session lookup first
86
+ let session = await getSessionState(args.taskId);
87
+ // Fall back to issue-based lookup
88
+ if (!session) {
89
+ session = await getSessionStateByIssue(args.taskId);
90
+ }
91
+ if (!session) {
92
+ return {
93
+ content: [{ type: 'text', text: `Error: No task found for ID "${args.taskId}"` }],
94
+ isError: true,
95
+ };
96
+ }
97
+ return {
98
+ content: [{ type: 'text', text: JSON.stringify(session, null, 2) }],
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
104
+ isError: true,
105
+ };
106
+ }
107
+ });
108
+ // ─── list-fleet ────────────────────────────────────────────────────
109
+ server.tool('list-fleet', 'List agents in the fleet with optional status filtering and result limiting.', {
110
+ status: z.array(z.enum(SESSION_STATUSES)).optional().describe('Filter by one or more statuses'),
111
+ limit: z.number().min(1).optional().describe('Maximum number of results to return (defaults to 20)'),
112
+ }, async (args) => {
113
+ try {
114
+ const limit = args.limit ?? 20;
115
+ let sessions;
116
+ if (args.status && args.status.length > 0) {
117
+ sessions = await getSessionsByStatus(args.status);
118
+ }
119
+ else {
120
+ sessions = await getAllSessions();
121
+ }
122
+ const limited = sessions.slice(0, limit);
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: JSON.stringify({
128
+ total: sessions.length,
129
+ returned: limited.length,
130
+ sessions: limited,
131
+ }, null, 2),
132
+ },
133
+ ],
134
+ };
135
+ }
136
+ catch (error) {
137
+ return {
138
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
139
+ isError: true,
140
+ };
141
+ }
142
+ });
143
+ // ─── get-cost-report ───────────────────────────────────────────────
144
+ server.tool('get-cost-report', 'Get cost and token usage report. If a taskId is provided, returns cost for that specific task. Otherwise, returns aggregate fleet costs.', {
145
+ taskId: z.string().optional().describe('Session ID or issue ID for a specific task (omit for fleet-wide report)'),
146
+ }, async (args) => {
147
+ try {
148
+ if (args.taskId) {
149
+ // Single-task cost report
150
+ let session = await getSessionState(args.taskId);
151
+ if (!session) {
152
+ session = await getSessionStateByIssue(args.taskId);
153
+ }
154
+ if (!session) {
155
+ return {
156
+ content: [{ type: 'text', text: `Error: No task found for ID "${args.taskId}"` }],
157
+ isError: true,
158
+ };
159
+ }
160
+ return {
161
+ content: [
162
+ {
163
+ type: 'text',
164
+ text: JSON.stringify({
165
+ taskId: session.linearSessionId,
166
+ issueId: session.issueId,
167
+ issueIdentifier: session.issueIdentifier,
168
+ status: session.status,
169
+ totalCostUsd: session.totalCostUsd ?? 0,
170
+ inputTokens: session.inputTokens ?? 0,
171
+ outputTokens: session.outputTokens ?? 0,
172
+ }, null, 2),
173
+ },
174
+ ],
175
+ };
176
+ }
177
+ // Fleet-wide cost report
178
+ const sessions = await getAllSessions();
179
+ let totalCostUsd = 0;
180
+ let totalInputTokens = 0;
181
+ let totalOutputTokens = 0;
182
+ let sessionsWithCost = 0;
183
+ for (const session of sessions) {
184
+ if (session.totalCostUsd != null) {
185
+ totalCostUsd += session.totalCostUsd;
186
+ sessionsWithCost++;
187
+ }
188
+ if (session.inputTokens != null) {
189
+ totalInputTokens += session.inputTokens;
190
+ }
191
+ if (session.outputTokens != null) {
192
+ totalOutputTokens += session.outputTokens;
193
+ }
194
+ }
195
+ return {
196
+ content: [
197
+ {
198
+ type: 'text',
199
+ text: JSON.stringify({
200
+ totalSessions: sessions.length,
201
+ sessionsWithCostData: sessionsWithCost,
202
+ totalCostUsd,
203
+ totalInputTokens,
204
+ totalOutputTokens,
205
+ }, null, 2),
206
+ },
207
+ ],
208
+ };
209
+ }
210
+ catch (error) {
211
+ return {
212
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
213
+ isError: true,
214
+ };
215
+ }
216
+ });
217
+ // ─── forward-prompt ────────────────────────────────────────────────
218
+ server.tool('forward-prompt', 'Forward a follow-up prompt to a running agent session. The prompt is queued and delivered to the agent via its message injection mechanism.', {
219
+ taskId: z.string().describe('Session ID or Linear issue ID of the running agent'),
220
+ message: z.string().describe('The follow-up prompt or message to send to the agent'),
221
+ }, async (args) => {
222
+ try {
223
+ // Resolve the session — try direct lookup, then issue-based
224
+ let session = await getSessionState(args.taskId);
225
+ if (!session) {
226
+ session = await getSessionStateByIssue(args.taskId);
227
+ }
228
+ if (!session) {
229
+ return {
230
+ content: [{ type: 'text', text: `Error: No task found for ID "${args.taskId}"` }],
231
+ isError: true,
232
+ };
233
+ }
234
+ // Only allow forwarding to active sessions
235
+ const forwardableStatuses = ['running', 'claimed'];
236
+ if (!forwardableStatuses.includes(session.status)) {
237
+ return {
238
+ content: [
239
+ {
240
+ type: 'text',
241
+ text: `Error: Task "${session.linearSessionId}" is in status "${session.status}". Prompts can only be forwarded to running or claimed sessions.`,
242
+ },
243
+ ],
244
+ isError: true,
245
+ };
246
+ }
247
+ const pending = await storePendingPrompt(session.linearSessionId, session.issueId, args.message);
248
+ if (!pending) {
249
+ return {
250
+ content: [{ type: 'text', text: `Error: Failed to store pending prompt. Redis may not be configured.` }],
251
+ isError: true,
252
+ };
253
+ }
254
+ return {
255
+ content: [
256
+ {
257
+ type: 'text',
258
+ text: JSON.stringify({
259
+ forwarded: true,
260
+ promptId: pending.id,
261
+ taskId: session.linearSessionId,
262
+ issueId: session.issueId,
263
+ sessionStatus: session.status,
264
+ }, null, 2),
265
+ },
266
+ ],
267
+ };
268
+ }
269
+ catch (error) {
270
+ return {
271
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
272
+ isError: true,
273
+ };
274
+ }
275
+ });
276
+ // ─── stop-agent ────────────────────────────────────────────────────
277
+ server.tool('stop-agent', 'Request to stop a running agent. Updates the session status to stopped.', {
278
+ taskId: z.string().describe('Session ID of the task to stop'),
279
+ }, async (args) => {
280
+ try {
281
+ // Verify the session exists and is in a stoppable state
282
+ let session = await getSessionState(args.taskId);
283
+ if (!session) {
284
+ session = await getSessionStateByIssue(args.taskId);
285
+ }
286
+ if (!session) {
287
+ return {
288
+ content: [{ type: 'text', text: `Error: No task found for ID "${args.taskId}"` }],
289
+ isError: true,
290
+ };
291
+ }
292
+ const stoppableStatuses = ['pending', 'claimed', 'running'];
293
+ if (!stoppableStatuses.includes(session.status)) {
294
+ return {
295
+ content: [
296
+ {
297
+ type: 'text',
298
+ text: `Error: Task "${session.linearSessionId}" is in status "${session.status}" and cannot be stopped. Only pending, claimed, or running tasks can be stopped.`,
299
+ },
300
+ ],
301
+ isError: true,
302
+ };
303
+ }
304
+ const updated = await updateSessionStatus(session.linearSessionId, 'stopped');
305
+ if (!updated) {
306
+ return {
307
+ content: [{ type: 'text', text: `Error: Failed to update task status. Redis may not be configured.` }],
308
+ isError: true,
309
+ };
310
+ }
311
+ return {
312
+ content: [
313
+ {
314
+ type: 'text',
315
+ text: JSON.stringify({
316
+ stopped: true,
317
+ taskId: session.linearSessionId,
318
+ issueId: session.issueId,
319
+ previousStatus: session.status,
320
+ newStatus: 'stopped',
321
+ }, null, 2),
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ catch (error) {
327
+ return {
328
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
329
+ isError: true,
330
+ };
331
+ }
332
+ });
333
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tools.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.test.d.ts","sourceRoot":"","sources":["../../src/tools.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,326 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ // ── Mock @renseiai/agentfactory-server ────────────────────────────────────────
3
+ vi.mock('@renseiai/agentfactory-server', () => ({
4
+ getAllSessions: vi.fn(),
5
+ getSessionsByStatus: vi.fn(),
6
+ getSessionState: vi.fn(),
7
+ getSessionStateByIssue: vi.fn(),
8
+ storeSessionState: vi.fn(),
9
+ updateSessionStatus: vi.fn(),
10
+ storePendingPrompt: vi.fn(),
11
+ }));
12
+ // ── Mock @modelcontextprotocol/sdk ──────────────────────────────────────────
13
+ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
14
+ McpServer: vi.fn(),
15
+ }));
16
+ import { getAllSessions, getSessionsByStatus, getSessionState, getSessionStateByIssue, storeSessionState, updateSessionStatus, storePendingPrompt, } from '@renseiai/agentfactory-server';
17
+ import { registerFleetTools } from './tools.js';
18
+ /** Capture tool registrations from registerFleetTools */
19
+ function captureTools() {
20
+ const tools = new Map();
21
+ const fakeMcpServer = {
22
+ tool: (name, _description, _schema, handler) => {
23
+ tools.set(name, handler);
24
+ },
25
+ };
26
+ registerFleetTools(fakeMcpServer);
27
+ return tools;
28
+ }
29
+ function makeSession(overrides = {}) {
30
+ return {
31
+ linearSessionId: 'ses-123',
32
+ issueId: 'issue-abc',
33
+ issueIdentifier: 'SUP-100',
34
+ providerSessionId: null,
35
+ worktreePath: '/tmp/wt',
36
+ status: 'running',
37
+ priority: 3,
38
+ workType: 'development',
39
+ queuedAt: 1000,
40
+ totalCostUsd: 0.5,
41
+ inputTokens: 1000,
42
+ outputTokens: 500,
43
+ ...overrides,
44
+ };
45
+ }
46
+ function parseResult(result) {
47
+ return JSON.parse(result.content[0].text);
48
+ }
49
+ // ── Tests ───────────────────────────────────────────────────────────────────
50
+ let tools;
51
+ beforeEach(() => {
52
+ vi.resetAllMocks();
53
+ tools = captureTools();
54
+ });
55
+ describe('registerFleetTools', () => {
56
+ it('registers all 6 expected tools', () => {
57
+ expect(tools.size).toBe(6);
58
+ expect([...tools.keys()].sort()).toEqual([
59
+ 'forward-prompt',
60
+ 'get-cost-report',
61
+ 'get-task-status',
62
+ 'list-fleet',
63
+ 'stop-agent',
64
+ 'submit-task',
65
+ ]);
66
+ });
67
+ });
68
+ describe('submit-task', () => {
69
+ it('creates a pending session and returns task info', async () => {
70
+ const handler = tools.get('submit-task');
71
+ vi.mocked(storeSessionState).mockResolvedValue({
72
+ linearSessionId: 'mcp-123-issue-1',
73
+ issueId: 'issue-1',
74
+ status: 'pending',
75
+ priority: 2,
76
+ workType: 'research',
77
+ });
78
+ const result = await handler({ issueId: 'issue-1', workType: 'research', priority: 2 });
79
+ const data = parseResult(result);
80
+ expect(data.submitted).toBe(true);
81
+ expect(data.issueId).toBe('issue-1');
82
+ expect(data.status).toBe('pending');
83
+ expect(data.workType).toBe('research');
84
+ expect(data.priority).toBe(2);
85
+ expect(storeSessionState).toHaveBeenCalledOnce();
86
+ });
87
+ it('defaults to development work type and priority 3', async () => {
88
+ const handler = tools.get('submit-task');
89
+ vi.mocked(storeSessionState).mockResolvedValue({
90
+ linearSessionId: 'mcp-123-issue-2',
91
+ issueId: 'issue-2',
92
+ status: 'pending',
93
+ priority: 3,
94
+ workType: 'development',
95
+ });
96
+ await handler({ issueId: 'issue-2' });
97
+ const callArgs = vi.mocked(storeSessionState).mock.calls[0][1];
98
+ expect(callArgs.workType).toBe('development');
99
+ expect(callArgs.priority).toBe(3);
100
+ });
101
+ it('returns error when storeSessionState throws', async () => {
102
+ const handler = tools.get('submit-task');
103
+ vi.mocked(storeSessionState).mockRejectedValue(new Error('Redis down'));
104
+ const result = await handler({ issueId: 'issue-1' });
105
+ expect(result.isError).toBe(true);
106
+ expect(result.content[0].text).toContain('Redis down');
107
+ });
108
+ });
109
+ describe('get-task-status', () => {
110
+ it('returns session when found by session ID', async () => {
111
+ const handler = tools.get('get-task-status');
112
+ const session = makeSession();
113
+ vi.mocked(getSessionState).mockResolvedValue(session);
114
+ const result = await handler({ taskId: 'ses-123' });
115
+ const data = parseResult(result);
116
+ expect(data.linearSessionId).toBe('ses-123');
117
+ expect(result.isError).toBeUndefined();
118
+ });
119
+ it('falls back to issue-based lookup', async () => {
120
+ const handler = tools.get('get-task-status');
121
+ vi.mocked(getSessionState).mockResolvedValue(null);
122
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(makeSession());
123
+ const result = await handler({ taskId: 'issue-abc' });
124
+ expect(getSessionStateByIssue).toHaveBeenCalledWith('issue-abc');
125
+ expect(result.isError).toBeUndefined();
126
+ });
127
+ it('returns error when task not found', async () => {
128
+ const handler = tools.get('get-task-status');
129
+ vi.mocked(getSessionState).mockResolvedValue(null);
130
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(null);
131
+ const result = await handler({ taskId: 'unknown' });
132
+ expect(result.isError).toBe(true);
133
+ expect(result.content[0].text).toContain('No task found');
134
+ });
135
+ });
136
+ describe('list-fleet', () => {
137
+ it('returns all sessions when no filter', async () => {
138
+ const handler = tools.get('list-fleet');
139
+ const sessions = [makeSession(), makeSession({ linearSessionId: 'ses-456' })];
140
+ vi.mocked(getAllSessions).mockResolvedValue(sessions);
141
+ const result = await handler({});
142
+ const data = parseResult(result);
143
+ expect(data.total).toBe(2);
144
+ expect(data.returned).toBe(2);
145
+ expect(getAllSessions).toHaveBeenCalledOnce();
146
+ });
147
+ it('filters by status', async () => {
148
+ const handler = tools.get('list-fleet');
149
+ vi.mocked(getSessionsByStatus).mockResolvedValue([makeSession()]);
150
+ const result = await handler({ status: ['running'] });
151
+ const data = parseResult(result);
152
+ expect(getSessionsByStatus).toHaveBeenCalledWith(['running']);
153
+ expect(data.total).toBe(1);
154
+ });
155
+ it('respects limit parameter', async () => {
156
+ const handler = tools.get('list-fleet');
157
+ const sessions = Array.from({ length: 30 }, (_, i) => makeSession({ linearSessionId: `ses-${i}` }));
158
+ vi.mocked(getAllSessions).mockResolvedValue(sessions);
159
+ const result = await handler({ limit: 5 });
160
+ const data = parseResult(result);
161
+ expect(data.total).toBe(30);
162
+ expect(data.returned).toBe(5);
163
+ });
164
+ it('defaults limit to 20', async () => {
165
+ const handler = tools.get('list-fleet');
166
+ const sessions = Array.from({ length: 25 }, (_, i) => makeSession({ linearSessionId: `ses-${i}` }));
167
+ vi.mocked(getAllSessions).mockResolvedValue(sessions);
168
+ const result = await handler({});
169
+ const data = parseResult(result);
170
+ expect(data.returned).toBe(20);
171
+ });
172
+ });
173
+ describe('get-cost-report', () => {
174
+ it('returns single-task cost when taskId provided', async () => {
175
+ const handler = tools.get('get-cost-report');
176
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ totalCostUsd: 1.23, inputTokens: 5000, outputTokens: 2000 }));
177
+ const result = await handler({ taskId: 'ses-123' });
178
+ const data = parseResult(result);
179
+ expect(data.totalCostUsd).toBe(1.23);
180
+ expect(data.inputTokens).toBe(5000);
181
+ expect(data.outputTokens).toBe(2000);
182
+ });
183
+ it('returns fleet-wide cost when no taskId', async () => {
184
+ const handler = tools.get('get-cost-report');
185
+ vi.mocked(getAllSessions).mockResolvedValue([
186
+ makeSession({ totalCostUsd: 1.0, inputTokens: 1000, outputTokens: 500 }),
187
+ makeSession({ totalCostUsd: 2.0, inputTokens: 2000, outputTokens: 1000 }),
188
+ ]);
189
+ const result = await handler({});
190
+ const data = parseResult(result);
191
+ expect(data.totalSessions).toBe(2);
192
+ expect(data.totalCostUsd).toBe(3.0);
193
+ expect(data.totalInputTokens).toBe(3000);
194
+ expect(data.totalOutputTokens).toBe(1500);
195
+ });
196
+ it('returns error when single task not found', async () => {
197
+ const handler = tools.get('get-cost-report');
198
+ vi.mocked(getSessionState).mockResolvedValue(null);
199
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(null);
200
+ const result = await handler({ taskId: 'unknown' });
201
+ expect(result.isError).toBe(true);
202
+ });
203
+ });
204
+ describe('stop-agent', () => {
205
+ it('stops a running task', async () => {
206
+ const handler = tools.get('stop-agent');
207
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }));
208
+ vi.mocked(updateSessionStatus).mockResolvedValue(true);
209
+ const result = await handler({ taskId: 'ses-123' });
210
+ const data = parseResult(result);
211
+ expect(data.stopped).toBe(true);
212
+ expect(data.previousStatus).toBe('running');
213
+ expect(data.newStatus).toBe('stopped');
214
+ expect(updateSessionStatus).toHaveBeenCalledWith('ses-123', 'stopped');
215
+ });
216
+ it('stops a pending task', async () => {
217
+ const handler = tools.get('stop-agent');
218
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'pending' }));
219
+ vi.mocked(updateSessionStatus).mockResolvedValue(true);
220
+ const result = await handler({ taskId: 'ses-123' });
221
+ const data = parseResult(result);
222
+ expect(data.stopped).toBe(true);
223
+ expect(data.previousStatus).toBe('pending');
224
+ });
225
+ it('rejects stopping a completed task', async () => {
226
+ const handler = tools.get('stop-agent');
227
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'completed' }));
228
+ const result = await handler({ taskId: 'ses-123' });
229
+ expect(result.isError).toBe(true);
230
+ expect(result.content[0].text).toContain('cannot be stopped');
231
+ });
232
+ it('returns error when task not found', async () => {
233
+ const handler = tools.get('stop-agent');
234
+ vi.mocked(getSessionState).mockResolvedValue(null);
235
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(null);
236
+ const result = await handler({ taskId: 'unknown' });
237
+ expect(result.isError).toBe(true);
238
+ expect(result.content[0].text).toContain('No task found');
239
+ });
240
+ it('returns error when Redis update fails', async () => {
241
+ const handler = tools.get('stop-agent');
242
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }));
243
+ vi.mocked(updateSessionStatus).mockResolvedValue(false);
244
+ const result = await handler({ taskId: 'ses-123' });
245
+ expect(result.isError).toBe(true);
246
+ expect(result.content[0].text).toContain('Failed to update');
247
+ });
248
+ });
249
+ describe('forward-prompt', () => {
250
+ it('forwards a prompt to a running session', async () => {
251
+ const handler = tools.get('forward-prompt');
252
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }));
253
+ vi.mocked(storePendingPrompt).mockResolvedValue({
254
+ id: 'prm_123',
255
+ sessionId: 'ses-123',
256
+ issueId: 'issue-abc',
257
+ prompt: 'Please also fix the tests',
258
+ createdAt: Date.now(),
259
+ });
260
+ const result = await handler({ taskId: 'ses-123', message: 'Please also fix the tests' });
261
+ const data = parseResult(result);
262
+ expect(data.forwarded).toBe(true);
263
+ expect(data.promptId).toBe('prm_123');
264
+ expect(data.taskId).toBe('ses-123');
265
+ expect(storePendingPrompt).toHaveBeenCalledWith('ses-123', 'issue-abc', 'Please also fix the tests');
266
+ });
267
+ it('forwards a prompt to a claimed session', async () => {
268
+ const handler = tools.get('forward-prompt');
269
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'claimed' }));
270
+ vi.mocked(storePendingPrompt).mockResolvedValue({
271
+ id: 'prm_456',
272
+ sessionId: 'ses-123',
273
+ issueId: 'issue-abc',
274
+ prompt: 'extra context',
275
+ createdAt: Date.now(),
276
+ });
277
+ const result = await handler({ taskId: 'ses-123', message: 'extra context' });
278
+ const data = parseResult(result);
279
+ expect(data.forwarded).toBe(true);
280
+ expect(data.sessionStatus).toBe('claimed');
281
+ });
282
+ it('falls back to issue-based lookup', async () => {
283
+ const handler = tools.get('forward-prompt');
284
+ vi.mocked(getSessionState).mockResolvedValue(null);
285
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(makeSession());
286
+ vi.mocked(storePendingPrompt).mockResolvedValue({
287
+ id: 'prm_789',
288
+ sessionId: 'ses-123',
289
+ issueId: 'issue-abc',
290
+ prompt: 'msg',
291
+ createdAt: Date.now(),
292
+ });
293
+ const result = await handler({ taskId: 'issue-abc', message: 'msg' });
294
+ expect(getSessionStateByIssue).toHaveBeenCalledWith('issue-abc');
295
+ expect(result.isError).toBeUndefined();
296
+ });
297
+ it('rejects forwarding to a completed session', async () => {
298
+ const handler = tools.get('forward-prompt');
299
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'completed' }));
300
+ const result = await handler({ taskId: 'ses-123', message: 'hello' });
301
+ expect(result.isError).toBe(true);
302
+ expect(result.content[0].text).toContain('Prompts can only be forwarded to running or claimed');
303
+ });
304
+ it('rejects forwarding to a stopped session', async () => {
305
+ const handler = tools.get('forward-prompt');
306
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'stopped' }));
307
+ const result = await handler({ taskId: 'ses-123', message: 'hello' });
308
+ expect(result.isError).toBe(true);
309
+ });
310
+ it('returns error when task not found', async () => {
311
+ const handler = tools.get('forward-prompt');
312
+ vi.mocked(getSessionState).mockResolvedValue(null);
313
+ vi.mocked(getSessionStateByIssue).mockResolvedValue(null);
314
+ const result = await handler({ taskId: 'unknown', message: 'hello' });
315
+ expect(result.isError).toBe(true);
316
+ expect(result.content[0].text).toContain('No task found');
317
+ });
318
+ it('returns error when storePendingPrompt fails', async () => {
319
+ const handler = tools.get('forward-prompt');
320
+ vi.mocked(getSessionState).mockResolvedValue(makeSession({ status: 'running' }));
321
+ vi.mocked(storePendingPrompt).mockResolvedValue(null);
322
+ const result = await handler({ taskId: 'ses-123', message: 'hello' });
323
+ expect(result.isError).toBe(true);
324
+ expect(result.content[0].text).toContain('Failed to store pending prompt');
325
+ });
326
+ });
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import http from 'node:http';
3
+ export interface HttpTransportOptions {
4
+ port?: number;
5
+ host?: string;
6
+ }
7
+ export declare function startStdioTransport(server: McpServer): Promise<void>;
8
+ export declare function startHttpTransport(server: McpServer, options?: HttpTransportOptions): Promise<http.Server>;
9
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CA0CtB"}
@@ -0,0 +1,44 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import http from 'node:http';
4
+ import { verifyMcpAuth, isMcpAuthConfigured } from './auth.js';
5
+ export async function startStdioTransport(server) {
6
+ const transport = new StdioServerTransport();
7
+ await server.connect(transport);
8
+ }
9
+ export async function startHttpTransport(server, options = {}) {
10
+ const port = options.port ?? parseInt(process.env.MCP_PORT ?? '3100', 10);
11
+ const host = options.host ?? process.env.MCP_HOST ?? '0.0.0.0';
12
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
13
+ const httpServer = http.createServer(async (req, res) => {
14
+ // Health check endpoint
15
+ if (req.url === '/health' && req.method === 'GET') {
16
+ res.writeHead(200, { 'Content-Type': 'application/json' });
17
+ res.end(JSON.stringify({ status: 'ok', server: 'agentfactory-fleet', auth: isMcpAuthConfigured() }));
18
+ return;
19
+ }
20
+ // MCP endpoint — verify auth for non-health endpoints
21
+ if (req.url === '/mcp') {
22
+ const authResult = verifyMcpAuth(req.headers.authorization);
23
+ if (!authResult.authorized) {
24
+ res.writeHead(401, { 'Content-Type': 'application/json' });
25
+ res.end(JSON.stringify({ error: authResult.error }));
26
+ return;
27
+ }
28
+ await transport.handleRequest(req, res);
29
+ return;
30
+ }
31
+ res.writeHead(404, { 'Content-Type': 'application/json' });
32
+ res.end(JSON.stringify({ error: 'Not found. Use /mcp for MCP protocol or /health for health check.' }));
33
+ });
34
+ await server.connect(transport);
35
+ return new Promise((resolve) => {
36
+ httpServer.listen(port, host, () => {
37
+ console.log(`[mcp-server] AgentFactory Fleet MCP server listening on http://${host}:${port}`);
38
+ console.log(`[mcp-server] MCP endpoint: http://${host}:${port}/mcp`);
39
+ console.log(`[mcp-server] Health check: http://${host}:${port}/health`);
40
+ console.log(`[mcp-server] Auth: ${isMcpAuthConfigured() ? 'enabled' : 'disabled (dev mode)'}`);
41
+ resolve(httpServer);
42
+ });
43
+ });
44
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@renseiai/agentfactory-mcp-server",
3
+ "version": "0.8.0",
4
+ "type": "module",
5
+ "description": "MCP server exposing AgentFactory fleet capabilities to MCP-aware clients",
6
+ "author": "Rensei AI (https://rensei.ai)",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22.0.0"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/renseiai/agentfactory",
14
+ "directory": "packages/mcp-server"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/src/index.d.ts",
21
+ "import": "./dist/src/index.js",
22
+ "default": "./dist/src/index.js"
23
+ }
24
+ },
25
+ "main": "./dist/src/index.js",
26
+ "types": "./dist/src/index.d.ts"
27
+ },
28
+ "main": "./dist/src/index.js",
29
+ "module": "./dist/src/index.js",
30
+ "types": "./dist/src/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./src/index.ts",
34
+ "import": "./dist/src/index.js",
35
+ "default": "./dist/src/index.js"
36
+ }
37
+ },
38
+ "bin": {
39
+ "af-mcp-server": "./dist/src/cli.js"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.1",
48
+ "@renseiai/agentfactory-server": "workspace:*",
49
+ "zod": "^4.3.6"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.5.4",
53
+ "typescript": "^5.7.3",
54
+ "vitest": "^3.2.3"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run --passWithNoTests",
60
+ "test:watch": "vitest",
61
+ "clean": "rm -rf dist",
62
+ "prepublishOnly": "pnpm clean && pnpm build"
63
+ }
64
+ }