When working with AI coding assistants on a large monorepo, one of the biggest challenges is context. The AI doesn't know which service a file belongs to, which test runner to use, or where the translation files live. It has to rediscover all of this on every interaction.
We solved this by building a custom Model Context Protocol (MCP) server that gives Claude deep, structured awareness of our multi-service e-commerce monorepo. In this article, we'll walk through the architecture, the tools we defined, and how we integrated it with the Claude Agent SDK to create autonomous bug-fixing workflows.
Prerequisites
- Node.js >= 18.x
- TypeScript with ESM support
- Familiarity with the Model Context Protocol
@modelcontextprotocol/sdkv1.27+
The Problem
Our e-commerce platform is a monorepo with four services: api (GraphQL backend), storefront (Next.js frontend), connector (data connector layer), and ml-pipeline (AI product classification). Each service has its own lint rules, test runner, and deployment pipeline.
When an AI agent tries to fix a bug, it needs to know:
- Which service does this file belong to?
- How do I run tests for just this service?
- Where are the translation files, and what format do they use?
- Which services are affected by my current changes?
Without this knowledge, the agent wastes turns asking questions or running the wrong commands. MCP solves this by exposing repository-specific tools that the agent can call directly.
Setting Up the MCP Server
We start with a basic MCP server using stdio transport. This is the standard pattern for tools that communicate with Claude through the Agent SDK:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { execSync } from 'node:child_process';
import { resolve } from 'node:path';
const REPO_ROOT = process.env.REPO_DIR || resolve(__dirname, '..', '..', '..');
const SERVICES = ['api', 'storefront', 'connector', 'ml-pipeline'];
const server = new McpServer({
name: 'monorepo-tools',
version: '1.0.0',
});
The McpServer class handles all the protocol plumbing. We define our tools on it, and then connect via stdio so the Claude Agent SDK can discover and call them.
Defining Tools
Each tool follows a consistent pattern: a name, description, a Zod schema for input validation, and an async handler that returns structured content. Let's look at the most important ones.
Service Detection
The simplest but most useful tool. Given a file path, it tells the agent which service it belongs to:
server.tool(
'detect_service',
'Given a file path, returns which service it belongs to',
{
filePath: z.string().describe('File path (relative or absolute)'),
},
async ({ filePath }) => {
const relative = filePath.replace(REPO_ROOT + '/', '');
const service = SERVICES.find((s) => relative.startsWith(s + '/'));
return {
content: [{
type: 'text',
text: service
? `Service: ${service}`
: `File does not belong to a known service`,
}],
};
},
);
This eliminates an entire class of mistakes where the agent tries to run frontend tests on a backend file, or vice versa.
Running Lint and Tests
Each service has different lint configs and test runners. We expose these as tools so the agent can validate its own changes:
server.tool(
'run_tests',
'Run tests for the specified service',
{
service: z.enum(['api', 'connector', 'ml-pipeline'])
.describe('Service to test'),
pattern: z.string().optional()
.describe('Test name pattern to filter'),
},
async ({ service, pattern }) => {
try {
const cmd = pattern
? `npm run test -- --test-name-pattern='${pattern}'`
: 'npm run test';
const output = execSync(cmd, {
encoding: 'utf-8',
cwd: resolve(REPO_ROOT, service),
timeout: 300_000,
});
return {
content: [{ type: 'text', text: `Tests passed for ${service}\n${output}` }],
};
} catch (e: any) {
return {
content: [{
type: 'text',
text: `Tests failed for ${service}\n${e.stdout || e.stderr || e.message}`,
}],
isError: true,
};
}
},
);
Notice the isError: true flag. When a tool returns an error, the agent knows the operation failed and can try a different approach rather than assuming success.
Translation Management
Our frontend supports three languages (German, French, Italian). Managing translations is tedious, and it's exactly the kind of task that benefits from tool support:
server.tool(
'add_translation',
'Add a new translation key to all three language files',
{
key: z.string().describe('Translation key'),
de: z.string().describe('German translation'),
fr: z.string().describe('French translation'),
it: z.string().describe('Italian translation'),
},
async ({ key, de, fr, it }) => {
const i18nDir = resolve(REPO_ROOT, 'storefront', 'public', 'i18n');
const translations = { de, fr, it };
const results: string[] = [];
for (const [lang, value] of Object.entries(translations)) {
const filePath = resolve(i18nDir, `${lang}.json`);
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
if (data[key]) {
results.push(`${lang}: key already exists (skipped)`);
continue;
}
data[key] = value;
const sorted = Object.fromEntries(
Object.entries(data).sort(([a], [b]) => a.localeCompare(b)),
);
writeFileSync(filePath, JSON.stringify(sorted, null, 2) + '\n');
results.push(`${lang}: added "${key}" = "${value}"`);
}
return { content: [{ type: 'text', text: results.join('\n') }] };
},
);
The tool sorts keys alphabetically (our project convention) and skips existing keys. This means the agent can safely call it without risking duplicates or breaking the key ordering that our team expects.
Affected Services Detection
The final tool inspects git diff to determine which services are affected by current changes. This is critical for the agent's verification step — it only needs to lint and test the services it actually touched:
server.tool(
'affected_services',
'Detect which services are affected by current git changes',
{},
async () => {
const diff = execSync('git diff --name-only HEAD', {
encoding: 'utf-8', cwd: REPO_ROOT,
}).trim();
const staged = execSync('git diff --name-only --cached', {
encoding: 'utf-8', cwd: REPO_ROOT,
}).trim();
const allFiles = [diff, staged].filter(Boolean).join('\n').split('\n');
const services = detectServicesFromFiles(allFiles);
return {
content: [{
type: 'text',
text: services.length > 0
? `Affected services: ${services.join(', ')}`
: 'No service-specific files changed',
}],
};
},
);
Starting the Server
The server connects via stdio, which is the standard transport for MCP servers that run as child processes of the Claude Agent SDK:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Configuration
The MCP server is registered in a .mcp.json file at the repository root. We also include a Playwright MCP server for frontend verification:
{
"mcpServers": {
"monorepo-tools": {
"command": "npx",
"args": ["tsx", "agent/src/tools/server.ts"]
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless", "--no-sandbox"]
}
}
}
Integration with Claude Agent SDK
On the agent side, we pass the MCP tools via the allowedTools parameter. This scopes each step of the workflow to only the tools it needs:
const result = await runAgenticStep(prompt, {
allowedTools: [
'Read', 'Edit', 'Write', 'Bash',
'mcp__monorepo-tools__run_lint',
'mcp__monorepo-tools__run_tests',
'mcp__monorepo-tools__detect_service',
'mcp__playwright__browser_navigate',
'mcp__playwright__browser_snapshot',
],
maxTurns: 25,
cwd: repoRoot,
});
The naming convention is mcp__{server-name}__{tool-name}. By controlling which tools are available at each step, we prevent the agent from running tests during the analysis phase or navigating the browser during the code implementation phase.
The Bug-Fix Pipeline
With these tools in place, we built a fully autonomous bug-fix pipeline:
- Analyze — The agent reads the issue description and uses
detect_serviceto understand which parts of the codebase are involved - Implement — The agent edits code and uses
find_similar_componentto follow existing patterns - Lint — The agent runs
run_linton affected services - Test — The agent runs
run_teststo verify the fix - Verify — The agent uses Playwright MCP tools to navigate the running app and visually confirm the fix
Each step is scoped to only the tools it needs, and the pipeline tracks costs per step for monitoring.
Conclusion
Building an MCP server for a monorepo is straightforward with the @modelcontextprotocol/sdk. The key insight is that the tools don't need to be sophisticated — even simple operations like "detect which service this file belongs to" dramatically improve the agent's accuracy. The real power comes from composing these simple tools into multi-step workflows through the Claude Agent SDK.