Skip to content

Commit c0a5227

Browse files
authored
Merge branch 'main' into ci/flake-check-paths-filter
2 parents ca04c09 + 46f31f3 commit c0a5227

File tree

6 files changed

+353
-279
lines changed

6 files changed

+353
-279
lines changed

mocks/handlers.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
import { http, HttpResponse } from 'msw';
2+
import { accountMcpTools, createMcpApp, defaultMcpTools, mixedProviderTools } from './mcp-server';
3+
4+
// Create MCP apps for testing
5+
const defaultMcpApp = createMcpApp({
6+
accountTools: {
7+
default: defaultMcpTools,
8+
acc1: accountMcpTools.acc1,
9+
acc2: accountMcpTools.acc2,
10+
acc3: accountMcpTools.acc3,
11+
'test-account': accountMcpTools['test-account'],
12+
mixed: mixedProviderTools,
13+
},
14+
});
215

316
// Helper to extract text content from OpenAI responses API input
417
const extractTextFromInput = (input: unknown): string => {
@@ -514,4 +527,13 @@ export const handlers = [
514527
});
515528
}),
516529

530+
// ============================================================
531+
// MCP Protocol endpoints (delegated to Hono app)
532+
// ============================================================
533+
http.all('https://api.stackone.com/mcp', async ({ request }) => {
534+
return defaultMcpApp.fetch(request);
535+
}),
536+
http.all('https://api.stackone-dev.com/mcp', async ({ request }) => {
537+
return defaultMcpApp.fetch(request);
538+
}),
517539
];

mocks/mcp-server.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Mock MCP server for testing using Hono's app.request() method.
3+
* This creates an MCP-compatible handler that can be used with MSW
4+
* without starting a real HTTP server.
5+
*/
6+
import type { Hono as HonoApp } from 'hono';
7+
import { http, HttpResponse } from 'msw';
8+
import { StreamableHTTPTransport } from '@hono/mcp';
9+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10+
import { Hono } from 'hono';
11+
import { basicAuth } from 'hono/basic-auth';
12+
13+
export interface McpToolDefinition {
14+
name: string;
15+
description?: string;
16+
inputSchema: {
17+
type: 'object';
18+
properties?: Record<string, unknown>;
19+
required?: string[];
20+
additionalProperties?: boolean;
21+
};
22+
}
23+
24+
export interface MockMcpServerOptions {
25+
/** Tools available per account ID. Use 'default' for tools when no account header is provided. */
26+
accountTools: Record<string, readonly McpToolDefinition[]>;
27+
}
28+
29+
/**
30+
* Creates an MSW handler for mocking MCP protocol requests.
31+
* Uses Hono's app.request() to handle requests without starting a server.
32+
*
33+
* @example
34+
* ```ts
35+
* import { server } from './mocks/node';
36+
* import { createMcpHandler, defaultMcpTools, accountMcpTools } from './mocks/mcp-server';
37+
*
38+
* // In your test setup
39+
* server.use(
40+
* createMcpHandler({
41+
* accountTools: {
42+
* default: defaultMcpTools,
43+
* 'account-1': accountMcpTools.acc1,
44+
* },
45+
* })
46+
* );
47+
* ```
48+
*/
49+
export function createMcpApp(options: MockMcpServerOptions): HonoApp {
50+
const { accountTools } = options;
51+
52+
// Create a Hono app that handles MCP protocol
53+
const app = new Hono();
54+
55+
// Apply Basic Auth middleware with hardcoded test credentials
56+
app.use(
57+
'/mcp',
58+
basicAuth({
59+
username: 'test-key',
60+
password: '',
61+
})
62+
);
63+
64+
app.all('/mcp', async (c) => {
65+
// Get account ID from header
66+
const accountId = c.req.header('x-account-id') ?? 'default';
67+
const tools = accountTools[accountId] ?? accountTools.default ?? [];
68+
69+
// Create a new MCP server instance per request
70+
const mcp = new McpServer({ name: 'test-mcp-server', version: '1.0.0' });
71+
const transport = new StreamableHTTPTransport();
72+
73+
for (const tool of tools) {
74+
mcp.registerTool(
75+
tool.name,
76+
{
77+
description: tool.description,
78+
// MCP SDK expects Zod-like schema but accepts JSON Schema objects
79+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK type mismatch
80+
inputSchema: tool.inputSchema as any,
81+
},
82+
async ({ params }: { params: { arguments?: Record<string, unknown> } }) => ({
83+
content: [],
84+
structuredContent: params.arguments ?? {},
85+
_meta: undefined,
86+
})
87+
);
88+
}
89+
90+
await mcp.connect(transport);
91+
return transport.handleRequest(c);
92+
});
93+
94+
return app;
95+
}
96+
97+
// Pre-defined tool sets for common test scenarios
98+
99+
export const defaultMcpTools = [
100+
{
101+
name: 'default_tool_1',
102+
description: 'Default Tool 1',
103+
inputSchema: {
104+
type: 'object',
105+
properties: { fields: { type: 'string' } },
106+
},
107+
},
108+
{
109+
name: 'default_tool_2',
110+
description: 'Default Tool 2',
111+
inputSchema: {
112+
type: 'object',
113+
properties: { id: { type: 'string' } },
114+
required: ['id'],
115+
},
116+
},
117+
] as const satisfies McpToolDefinition[];
118+
119+
export const accountMcpTools = {
120+
acc1: [
121+
{
122+
name: 'acc1_tool_1',
123+
description: 'Account 1 Tool 1',
124+
inputSchema: {
125+
type: 'object',
126+
properties: { fields: { type: 'string' } },
127+
},
128+
},
129+
{
130+
name: 'acc1_tool_2',
131+
description: 'Account 1 Tool 2',
132+
inputSchema: {
133+
type: 'object',
134+
properties: { id: { type: 'string' } },
135+
required: ['id'],
136+
},
137+
},
138+
] as const satisfies McpToolDefinition[],
139+
acc2: [
140+
{
141+
name: 'acc2_tool_1',
142+
description: 'Account 2 Tool 1',
143+
inputSchema: {
144+
type: 'object',
145+
properties: { fields: { type: 'string' } },
146+
},
147+
},
148+
{
149+
name: 'acc2_tool_2',
150+
description: 'Account 2 Tool 2',
151+
inputSchema: {
152+
type: 'object',
153+
properties: { id: { type: 'string' } },
154+
required: ['id'],
155+
},
156+
},
157+
] as const satisfies McpToolDefinition[],
158+
acc3: [
159+
{
160+
name: 'acc3_tool_1',
161+
description: 'Account 3 Tool 1',
162+
inputSchema: {
163+
type: 'object',
164+
properties: { fields: { type: 'string' } },
165+
},
166+
},
167+
] as const satisfies McpToolDefinition[],
168+
'test-account': [
169+
{
170+
name: 'dummy_action',
171+
description: 'Dummy tool',
172+
inputSchema: {
173+
type: 'object',
174+
properties: {
175+
foo: {
176+
type: 'string',
177+
description: 'A string parameter',
178+
},
179+
},
180+
required: ['foo'],
181+
additionalProperties: false,
182+
},
183+
},
184+
] as const satisfies McpToolDefinition[],
185+
} as const;
186+
187+
export const mixedProviderTools = [
188+
{
189+
name: 'hibob_list_employees',
190+
description: 'HiBob List Employees',
191+
inputSchema: {
192+
type: 'object',
193+
properties: { fields: { type: 'string' } },
194+
},
195+
},
196+
{
197+
name: 'hibob_create_employees',
198+
description: 'HiBob Create Employees',
199+
inputSchema: {
200+
type: 'object',
201+
properties: { name: { type: 'string' } },
202+
required: ['name'],
203+
},
204+
},
205+
{
206+
name: 'bamboohr_list_employees',
207+
description: 'BambooHR List Employees',
208+
inputSchema: {
209+
type: 'object',
210+
properties: { fields: { type: 'string' } },
211+
},
212+
},
213+
{
214+
name: 'bamboohr_get_employee',
215+
description: 'BambooHR Get Employee',
216+
inputSchema: {
217+
type: 'object',
218+
properties: { id: { type: 'string' } },
219+
required: ['id'],
220+
},
221+
},
222+
{
223+
name: 'workday_list_employees',
224+
description: 'Workday List Employees',
225+
inputSchema: {
226+
type: 'object',
227+
properties: { fields: { type: 'string' } },
228+
},
229+
},
230+
] as const satisfies McpToolDefinition[];

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ catalogMode: strict
66

77
catalogs:
88
dev:
9-
'@ai-sdk/openai': ^2.0.80
10-
'@ai-sdk/provider-utils': ^3.0.18
11-
'@biomejs/biome': ^1.5.3
12-
'@hono/mcp': ^0.1.4
13-
'@types/json-schema': ^7.0.15
14-
'@types/node': ^22.13.5
15-
'@typescript/native-preview': ^7.0.0-dev.20251202.1
16-
'@vitest/coverage-v8': ^4.0.15
9+
"@ai-sdk/openai": ^2.0.80
10+
"@ai-sdk/provider-utils": ^3.0.18
11+
"@biomejs/biome": ^1.5.3
12+
"@hono/mcp": ^0.1.4
13+
"@types/json-schema": ^7.0.15
14+
"@types/node": ^22.13.5
15+
"@typescript/native-preview": ^7.0.0-dev.20251202.1
16+
"@vitest/coverage-v8": ^4.0.15
1717
hono: ^4.9.10
1818
knip: ^5.72.0
1919
lefthook: ^2.0.8
@@ -29,16 +29,16 @@ catalogs:
2929
ai: ^5.0.108
3030
openai: ^6.2.0
3131
prod:
32-
'@modelcontextprotocol/sdk': ^1.24.3
33-
'@orama/orama': ^3.1.11
32+
"@modelcontextprotocol/sdk": ^1.24.3
33+
"@orama/orama": ^3.1.11
3434
json-schema: ^0.4.0
3535

3636
enablePrePostScripts: true
3737

3838
minimumReleaseAge: 1440
3939

4040
onlyBuiltDependencies:
41-
- '@biomejs/biome'
41+
- "@biomejs/biome"
4242
- esbuild
4343
- lefthook
4444
- msw

0 commit comments

Comments
 (0)