Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e14f2b8
docs(specs): analyze repository and define mcp-server spec 077
arielshad Mar 23, 2026
9fd76a0
docs(specs): define requirements and product questions for mcp-server…
arielshad Mar 23, 2026
f6cf91f
docs(specs): create implementation plan and task breakdown for mcp-se…
arielshad Mar 23, 2026
a1ba7b3
feat(deps): add @modelcontextprotocol/sdk dependency for mcp server
arielshad Mar 23, 2026
168ab39
feat(api): add mcp server service core with stdio transport
arielshad Mar 23, 2026
dc784b0
feat(api): implement list_features mcp tool with zod schema
arielshad Mar 23, 2026
b16e197
feat(api): register mcp server factory in di container
arielshad Mar 23, 2026
aa3b31c
feat(api): add show_feature, create_feature, and start_feature mcp tools
arielshad Mar 23, 2026
0acba58
feat(api): add agent-domain mcp tools
arielshad Mar 23, 2026
8189f6a
feat(api): add list_repositories mcp tool
arielshad Mar 23, 2026
f588a87
feat(api): add get_settings and update_settings mcp tools
arielshad Mar 23, 2026
2f4221b
feat(api): wire all mcp tool groups into register all tools barrel
arielshad Mar 23, 2026
d7675fa
feat(cli): add shep mcp command for launching mcp server
arielshad Mar 23, 2026
6a01de1
feat(cli): register mcp command in cli entry point
arielshad Mar 23, 2026
e4efcf4
test(cli): add stdio safety and console.log absence tests for mcp
arielshad Mar 23, 2026
e5162aa
test(api): add mcp protocol round-trip integration tests
arielshad Mar 23, 2026
1c04dd7
chore(specs): capture evidence for mcp-server feature
arielshad Mar 23, 2026
48ec68c
chore(specs): capture evidence for mcp-server feature
arielshad Mar 23, 2026
be1d52e
chore(specs): capture evidence for mcp-server feature
arielshad Mar 23, 2026
1c41c26
chore(specs): add feature and research artifacts for mcp-server spec
arielshad Mar 23, 2026
03f6101
chore(specs): update mcp-server feature timestamp
arielshad Mar 24, 2026
830999e
Merge remote-tracking branch 'origin/main' into feat/mcp-server
arielshad Mar 24, 2026
88fe332
chore(specs): update mcp-server spec with rejection feedback
arielshad Mar 24, 2026
9926128
chore(specs): update mcp-server feature timestamp after merge resolution
arielshad Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"@types/node-notifier": "^8.0.5",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/which": "^3.0.4",
"@typespec-tools/emitter-typescript": "^0.3.0",
"@typespec/compiler": "^0.60.0",
"@typespec/json-schema": "^0.60.0",
Expand Down Expand Up @@ -178,6 +179,7 @@
"@langchain/core": "^1.1.22",
"@langchain/langgraph": "^1.1.4",
"@langchain/langgraph-checkpoint-sqlite": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.27.1",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"better-sqlite3": "^12.6.2",
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/infrastructure/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,20 @@ export async function initializeContainer(): Promise<typeof container> {
useFactory: (c) => c.resolve(GetBranchSyncStatusUseCase),
});

// McpServerFactory is registered as a lazy async factory to avoid importing
// @modelcontextprotocol/sdk for non-MCP commands. The factory uses dynamic
// import() — the actual SDK import only happens when the factory is called.
container.register('McpServerFactory', {
useFactory: (c) => {
return async () => {
const { McpServerService } = await import('../services/mcp/mcp-server.service.js');
const versionService = c.resolve<IVersionService>('IVersionService');
const { version } = versionService.getVersion();
return new McpServerService(version, c);
};
},
});

_initialized = true;
return container;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* MCP Server Service
*
* Thin presentation adapter that exposes shep use cases as MCP tools.
* Uses the official @modelcontextprotocol/sdk with stdio transport.
*
* Architecture: This sits in the infrastructure layer as a presentation adapter,
* the same pattern used by CLI commands and the web server. It resolves use cases
* from the shared DI container and contains zero business logic.
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import type DependencyContainer from 'tsyringe/dist/typings/types/dependency-container.js';
import { registerAllTools } from './tools/index.js';

export class McpServerService {
public readonly server: McpServer;

constructor(version: string, container?: DependencyContainer) {
this.server = new McpServer({
name: 'shep',
version,
});

if (container) {
registerAllTools(this.server, container);
}
}

/**
* Connect the MCP server to a stdio transport and start listening.
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}

/**
* Close the MCP server and transport.
*/
async stop(): Promise<void> {
await this.server.close();
}
}
118 changes: 118 additions & 0 deletions packages/core/src/infrastructure/services/mcp/tools/agent-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* MCP Agent Tools
*
* Registers agent-related MCP tools on the server.
* Each tool is a thin adapter that delegates to a use case.
*/

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type DependencyContainer from 'tsyringe/dist/typings/types/dependency-container.js';
import { z } from 'zod';
import { RunAgentUseCase } from '../../../../application/use-cases/agents/run-agent.use-case.js';
import { GetAgentRunUseCase } from '../../../../application/use-cases/agents/get-agent-run.use-case.js';
import { ListAgentRunsUseCase } from '../../../../application/use-cases/agents/list-agent-runs.use-case.js';
import { StopAgentRunUseCase } from '../../../../application/use-cases/agents/stop-agent-run.use-case.js';

/**
* Wraps an async handler in try/catch, returning MCP error responses on failure.
*/
async function withErrorHandling(
fn: () => Promise<{ content: { type: 'text'; text: string }[]; isError?: boolean }>
): Promise<{ content: { type: 'text'; text: string }[]; isError?: boolean }> {
try {
return await fn();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: message }],
isError: true,
};
}
}

/**
* Register agent-related MCP tools on the server.
*/
export function registerAgentTools(server: McpServer, container: DependencyContainer): void {
server.registerTool(
'run_agent',
{
description:
'Run a named agent with a prompt. Returns the agent run ID immediately without blocking — use show_agent_run to poll for status.',
inputSchema: {
agentName: z.string().describe('Name of the agent to run'),
prompt: z.string().describe('Prompt or instructions for the agent'),
},
},
async ({ agentName, prompt }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(RunAgentUseCase);
const agentRun = await useCase.execute({ agentName, prompt });
return {
content: [{ type: 'text' as const, text: JSON.stringify(agentRun, null, 2) }],
};
});
}
);

server.registerTool(
'show_agent_run',
{
description: 'Get the status and details of an agent run by its ID.',
inputSchema: {
runId: z.string().describe('Agent run ID'),
},
},
async ({ runId }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(GetAgentRunUseCase);
const agentRun = await useCase.execute(runId);
if (!agentRun) {
return {
content: [{ type: 'text' as const, text: `Agent run not found: "${runId}"` }],
isError: true,
};
}
return {
content: [{ type: 'text' as const, text: JSON.stringify(agentRun, null, 2) }],
};
});
}
);

server.registerTool(
'list_agent_runs',
{
description: 'List all agent runs, sorted by most recent first.',
inputSchema: {},
},
async () => {
return withErrorHandling(async () => {
const useCase = container.resolve(ListAgentRunsUseCase);
const runs = await useCase.execute();
return {
content: [{ type: 'text' as const, text: JSON.stringify(runs, null, 2) }],
};
});
}
);

server.registerTool(
'stop_agent_run',
{
description: 'Stop a running agent by its run ID. Returns whether the stop was successful.',
inputSchema: {
runId: z.string().describe('Agent run ID to stop'),
},
},
async ({ runId }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(StopAgentRunUseCase);
const result = await useCase.execute(runId);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
});
}
);
}
152 changes: 152 additions & 0 deletions packages/core/src/infrastructure/services/mcp/tools/feature-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* MCP Feature Tools
*
* Registers feature-related MCP tools on the server.
* Each tool is a thin adapter that delegates to a use case.
*/

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type DependencyContainer from 'tsyringe/dist/typings/types/dependency-container.js';
import { z } from 'zod';
import { ListFeaturesUseCase } from '../../../../application/use-cases/features/list-features.use-case.js';
import { ShowFeatureUseCase } from '../../../../application/use-cases/features/show-feature.use-case.js';
import { CreateFeatureUseCase } from '../../../../application/use-cases/features/create/create-feature.use-case.js';
import { StartFeatureUseCase } from '../../../../application/use-cases/features/start-feature.use-case.js';
import { SdlcLifecycle } from '../../../../domain/generated/output.js';
import type { FeatureListFilters } from '../../../../application/ports/output/repositories/feature-repository.interface.js';

/**
* Wraps an async handler in try/catch, returning MCP error responses on failure.
*/
async function withErrorHandling(
fn: () => Promise<{ content: { type: 'text'; text: string }[]; isError?: boolean }>
): Promise<{ content: { type: 'text'; text: string }[]; isError?: boolean }> {
try {
return await fn();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: message }],
isError: true,
};
}
}

/**
* Register feature-related MCP tools on the server.
*/
export function registerFeatureTools(server: McpServer, container: DependencyContainer): void {
server.registerTool(
'list_features',
{
description:
'List all features tracked by shep. Optionally filter by lifecycle status (e.g. Pending, Implementation, Review).',
inputSchema: {
status: z
.enum([
SdlcLifecycle.Started,
SdlcLifecycle.Analyze,
SdlcLifecycle.Requirements,
SdlcLifecycle.Research,
SdlcLifecycle.Planning,
SdlcLifecycle.Implementation,
SdlcLifecycle.Review,
SdlcLifecycle.Maintain,
SdlcLifecycle.Blocked,
SdlcLifecycle.Pending,
])
.optional()
.describe('Filter by lifecycle status'),
},
},
async ({ status }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(ListFeaturesUseCase);
const filters: FeatureListFilters = {};
if (status) {
filters.lifecycle = status as SdlcLifecycle;
}
const features = await useCase.execute(filters);
return {
content: [{ type: 'text' as const, text: JSON.stringify(features, null, 2) }],
};
});
}
);

server.registerTool(
'show_feature',
{
description:
'Get detailed information about a feature by ID. Supports prefix matching — you can provide the first few characters of the feature ID.',
inputSchema: {
featureId: z.string().describe('Feature ID or ID prefix'),
},
},
async ({ featureId }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(ShowFeatureUseCase);
const feature = await useCase.execute(featureId);
return {
content: [{ type: 'text' as const, text: JSON.stringify(feature, null, 2) }],
};
});
}
);

server.registerTool(
'create_feature',
{
description:
'Create a new feature in shep. Requires a user input describing the feature and the repository path. Optionally provide a name and description to skip AI metadata extraction.',
inputSchema: {
userInput: z.string().describe('Natural language description of the feature to create'),
repositoryPath: z.string().describe('Absolute path to the repository'),
name: z.string().optional().describe('Pre-supplied feature name (skips AI extraction)'),
description: z
.string()
.optional()
.describe('Pre-supplied feature description (skips AI extraction)'),
pending: z
.boolean()
.optional()
.describe('When true, create in Pending state without spawning an agent'),
},
},
async ({ userInput, repositoryPath, name, description, pending }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(CreateFeatureUseCase);
const result = await useCase.execute({
userInput,
repositoryPath,
name,
description,
pending,
});
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
});
}
);

server.registerTool(
'start_feature',
{
description:
'Start a pending feature. Triggers an agent run and returns the run ID immediately without blocking.',
inputSchema: {
featureId: z.string().describe('ID of the pending feature to start'),
},
},
async ({ featureId }) => {
return withErrorHandling(async () => {
const useCase = container.resolve(StartFeatureUseCase);
const result = await useCase.execute(featureId);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
});
}
);
}
22 changes: 22 additions & 0 deletions packages/core/src/infrastructure/services/mcp/tools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* MCP Tools Registration
*
* Barrel module that registers all MCP tools on a server instance.
*/

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type DependencyContainer from 'tsyringe/dist/typings/types/dependency-container.js';
import { registerFeatureTools } from './feature-tools.js';
import { registerAgentTools } from './agent-tools.js';
import { registerRepoTools } from './repo-tools.js';
import { registerSettingsTools } from './settings-tools.js';

/**
* Register all MCP tools on the server.
*/
export function registerAllTools(server: McpServer, container: DependencyContainer): void {
registerFeatureTools(server, container);
registerAgentTools(server, container);
registerRepoTools(server, container);
registerSettingsTools(server, container);
}
Loading
Loading