handoff-app 2.0.0-11 → 2.0.0-12
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.
- package/dist/app-builder/build.js +2 -2
- package/package.json +2 -3
- package/src/app/lib/mcp/components.ts +33 -29
- package/src/app/lib/mcp/pages.ts +26 -19
- package/src/app/lib/mcp/server.ts +33 -9
- package/src/app/lib/mcp/tokens.ts +22 -16
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app-builder/build.ts +2 -2
|
@@ -510,7 +510,7 @@ containers, custom Node servers, and other non-Vercel hosts.
|
|
|
510
510
|
* Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
|
|
511
511
|
* runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
|
|
512
512
|
* runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
|
|
513
|
-
*
|
|
513
|
+
* is only needed when `/api/mcp/` is enabled; the driver package is
|
|
514
514
|
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
|
|
515
515
|
* SMTP email provider needs `nodemailer`.
|
|
516
516
|
*/
|
|
@@ -520,7 +520,7 @@ const getRequiredRegistryRuntimeModules = (handoff) => {
|
|
|
520
520
|
// The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
|
|
521
521
|
// endpoint is served.
|
|
522
522
|
if ((0, config_1.isMcpEnabled)(handoff.config) || ai.enabled) {
|
|
523
|
-
base.push('@modelcontextprotocol/sdk'
|
|
523
|
+
base.push('@modelcontextprotocol/sdk');
|
|
524
524
|
}
|
|
525
525
|
if (ai.enabled) {
|
|
526
526
|
base.push('ai', '@ai-sdk/openai-compatible');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-app",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-12",
|
|
4
4
|
"description": "Automated documentation toolchain for building client side documentation from figma",
|
|
5
5
|
"author ": {
|
|
6
6
|
"name": "Convertiv",
|
|
@@ -167,8 +167,7 @@
|
|
|
167
167
|
"vaul": "^1.1.2",
|
|
168
168
|
"vite": "^6.3.5",
|
|
169
169
|
"ws": "^8.18.3",
|
|
170
|
-
"yargs": "^17.7.2"
|
|
171
|
-
"zod": "^4.4.3"
|
|
170
|
+
"yargs": "^17.7.2"
|
|
172
171
|
},
|
|
173
172
|
"peerDependencies": {
|
|
174
173
|
"typescript": ">=5.6 <7"
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
1
|
import { buildArtifactUrl } from '@handoff/artifacts/url';
|
|
4
2
|
import type { SlotMetadata } from '@handoff/transformers/preview/component';
|
|
5
3
|
import type { ComponentListObject, OptionalPreviewRender, TransformComponentTokensResult } from '@handoff/transformers/preview/types';
|
|
6
4
|
import type { DocsBackend } from '../docs-api/backend';
|
|
7
5
|
import { basePath, fail, ok, read } from './result';
|
|
6
|
+
import { defineTool, type McpTool } from './tool';
|
|
8
7
|
|
|
9
8
|
/** The component tools: search, read, one preview, and the source. */
|
|
10
9
|
|
|
@@ -16,10 +15,12 @@ const MAX_SEARCH_LIMIT = 100;
|
|
|
16
15
|
const SOURCE_FIELDS = ['code', 'css', 'sass', 'js'] as const;
|
|
17
16
|
type SourceField = (typeof SOURCE_FIELDS)[number];
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const componentId = { type: 'string', description: 'Component id, as returned by handoff_search_components.' };
|
|
19
|
+
|
|
20
|
+
export const componentTools: McpTool[] = [
|
|
21
|
+
defineTool<{ query?: string; group?: string; category?: string; tag?: string; limit?: number }>(
|
|
22
22
|
{
|
|
23
|
+
name: 'handoff_search_components',
|
|
23
24
|
title: 'Search components',
|
|
24
25
|
description:
|
|
25
26
|
'Find components. `query` matches id, title, group, categories and tags. With no arguments, ' +
|
|
@@ -27,11 +28,14 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
27
28
|
'tags. Read one component with handoff_get_component.',
|
|
28
29
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
29
30
|
inputSchema: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
query: { type: 'string', description: 'Text to find. Case-insensitive substring match.' },
|
|
34
|
+
group: { type: 'string', description: 'Group name, for example "Form Elements". Exact match, case-insensitive.' },
|
|
35
|
+
category: { type: 'string', description: 'Category name. Exact match, case-insensitive.' },
|
|
36
|
+
tag: { type: 'string', description: 'Tag name. Exact match, case-insensitive.' },
|
|
37
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX_SEARCH_LIMIT, description: `Max results (default ${DEFAULT_SEARCH_LIMIT}).` },
|
|
38
|
+
},
|
|
35
39
|
},
|
|
36
40
|
},
|
|
37
41
|
async ({ query, group, category, tag, limit }) =>
|
|
@@ -42,11 +46,11 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
42
46
|
const components = matched.slice(0, limit ?? DEFAULT_SEARCH_LIMIT).map(toComponentSummary);
|
|
43
47
|
return ok({ total: matched.length, returned: components.length, components });
|
|
44
48
|
})
|
|
45
|
-
)
|
|
49
|
+
),
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
'handoff_get_component',
|
|
51
|
+
defineTool<{ id: string }>(
|
|
49
52
|
{
|
|
53
|
+
name: 'handoff_get_component',
|
|
50
54
|
title: 'Get component',
|
|
51
55
|
description:
|
|
52
56
|
'One component: properties, variants, usage and guidelines. `usage` is the snippet of the first ' +
|
|
@@ -56,9 +60,7 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
56
60
|
'handoff_get_component_preview. `tokens.set`, when present, is the id to pass to ' +
|
|
57
61
|
'handoff_get_tokens. Do not derive a token set id from the component id.',
|
|
58
62
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
59
|
-
inputSchema: {
|
|
60
|
-
id: z.string().describe('Component id, as returned by handoff_search_components.'),
|
|
61
|
-
},
|
|
63
|
+
inputSchema: { type: 'object', properties: { id: componentId }, required: ['id'] },
|
|
62
64
|
},
|
|
63
65
|
async ({ id }) =>
|
|
64
66
|
read(`component "${id}"`, async (backend) => {
|
|
@@ -76,19 +78,23 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
76
78
|
)
|
|
77
79
|
);
|
|
78
80
|
})
|
|
79
|
-
)
|
|
81
|
+
),
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
'handoff_get_component_preview',
|
|
83
|
+
defineTool<{ id: string; preview: string }>(
|
|
83
84
|
{
|
|
85
|
+
name: 'handoff_get_component_preview',
|
|
84
86
|
title: 'Get component preview',
|
|
85
87
|
description:
|
|
86
88
|
'One state of a component: `values` (the args), `usage` (the snippet) and `html` (the rendered ' +
|
|
87
89
|
'markup). `html` is absent when the component is not built.',
|
|
88
90
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
89
91
|
inputSchema: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
id: componentId,
|
|
95
|
+
preview: { type: 'string', description: 'Preview id, as listed in `previews[].id` by handoff_get_component.' },
|
|
96
|
+
},
|
|
97
|
+
required: ['id', 'preview'],
|
|
92
98
|
},
|
|
93
99
|
},
|
|
94
100
|
async ({ id, preview }) =>
|
|
@@ -108,20 +114,18 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
108
114
|
}
|
|
109
115
|
return ok(toPreviewResult(record, preview, found, basePath(), await readPreviewDocument(backend, id, preview)));
|
|
110
116
|
})
|
|
111
|
-
)
|
|
117
|
+
),
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
'handoff_get_component_source',
|
|
119
|
+
defineTool<{ id: string }>(
|
|
115
120
|
{
|
|
121
|
+
name: 'handoff_get_component_source',
|
|
116
122
|
title: 'Get component source',
|
|
117
123
|
description:
|
|
118
124
|
'The implementation of a component: `code` (the component or template) and, when present, ' +
|
|
119
125
|
'`css`, `sass` and `js`. Use it only when handoff_get_component is not enough. The source ' +
|
|
120
126
|
'fields are absent when the component is not built.',
|
|
121
127
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
122
|
-
inputSchema: {
|
|
123
|
-
id: z.string().describe('Component id, as returned by handoff_search_components.'),
|
|
124
|
-
},
|
|
128
|
+
inputSchema: { type: 'object', properties: { id: componentId }, required: ['id'] },
|
|
125
129
|
},
|
|
126
130
|
async ({ id }) =>
|
|
127
131
|
read(`source of component "${id}"`, async (backend) => {
|
|
@@ -131,8 +135,8 @@ export const registerComponentTools = (server: McpServer): void => {
|
|
|
131
135
|
}
|
|
132
136
|
return ok(toComponentSource(record, await readComponentArtifact(backend, id)));
|
|
133
137
|
})
|
|
134
|
-
)
|
|
135
|
-
|
|
138
|
+
),
|
|
139
|
+
];
|
|
136
140
|
|
|
137
141
|
const componentNotFound = (id: string) =>
|
|
138
142
|
fail(`Component "${id}" was not found. Use handoff_search_components to list the components that exist.`);
|
package/src/app/lib/mcp/pages.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
1
|
import type { PageDetail } from '../docs-api/records';
|
|
4
2
|
import { createSearchRequest, DEFAULT_RESULT_LIMIT, MAX_QUERY_LENGTH, MAX_RESULT_LIMIT, MAX_TERMS } from '../docs-api/search';
|
|
5
3
|
import { pageIdFromUrl } from './page-url';
|
|
6
4
|
import { fail, ok, read } from './result';
|
|
5
|
+
import { defineTool, type McpTool } from './tool';
|
|
7
6
|
|
|
8
7
|
/** The documentation page tools: search and read. */
|
|
9
8
|
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
'handoff_search_pages',
|
|
9
|
+
export const pageTools: McpTool[] = [
|
|
10
|
+
defineTool<{ query: string; group?: string; limit?: number }>(
|
|
13
11
|
{
|
|
12
|
+
name: 'handoff_search_pages',
|
|
14
13
|
title: 'Search pages',
|
|
15
14
|
description:
|
|
16
15
|
'Search documentation pages by title, description and Markdown body. Returns ranked `results` ' +
|
|
@@ -18,14 +17,18 @@ export const registerPageTools = (server: McpServer): void => {
|
|
|
18
17
|
'Read a page with handoff_get_page.',
|
|
19
18
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
20
19
|
inputSchema: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
query: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description:
|
|
25
|
+
`Search text, at most ${MAX_QUERY_LENGTH} characters and ${MAX_TERMS} terms. Case-insensitive. ` +
|
|
26
|
+
'Terms shorter than two characters are ignored.',
|
|
27
|
+
},
|
|
28
|
+
group: { type: 'string', description: 'Page group name. Exact match, case-insensitive.' },
|
|
29
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX_RESULT_LIMIT, description: `Max results (default ${DEFAULT_RESULT_LIMIT}).` },
|
|
30
|
+
},
|
|
31
|
+
required: ['query'],
|
|
29
32
|
},
|
|
30
33
|
},
|
|
31
34
|
async ({ query, group, limit }) => {
|
|
@@ -35,11 +38,11 @@ export const registerPageTools = (server: McpServer): void => {
|
|
|
35
38
|
}
|
|
36
39
|
return read('pages', async (backend) => ok(await backend.searchPages({ ...parsed.request, group: group?.trim() || undefined })));
|
|
37
40
|
}
|
|
38
|
-
)
|
|
41
|
+
),
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
'handoff_get_page',
|
|
43
|
+
defineTool<{ url: string }>(
|
|
42
44
|
{
|
|
45
|
+
name: 'handoff_get_page',
|
|
43
46
|
title: 'Get page',
|
|
44
47
|
description:
|
|
45
48
|
'One documentation page by its URL from handoff_search_pages, for example /guides/setup, or / ' +
|
|
@@ -47,7 +50,11 @@ export const registerPageTools = (server: McpServer): void => {
|
|
|
47
50
|
'external-link page, the external site is not fetched.',
|
|
48
51
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
49
52
|
inputSchema: {
|
|
50
|
-
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
url: { type: 'string', description: 'Internal route, without a base path, query string or fragment.' },
|
|
56
|
+
},
|
|
57
|
+
required: ['url'],
|
|
51
58
|
},
|
|
52
59
|
},
|
|
53
60
|
async ({ url }) => {
|
|
@@ -63,8 +70,8 @@ export const registerPageTools = (server: McpServer): void => {
|
|
|
63
70
|
return ok(toPageResult(page));
|
|
64
71
|
});
|
|
65
72
|
}
|
|
66
|
-
)
|
|
67
|
-
|
|
73
|
+
),
|
|
74
|
+
];
|
|
68
75
|
|
|
69
76
|
export const toPageResult = (page: PageDetail) => ({
|
|
70
77
|
id: page.id,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
|
|
4
|
+
import { componentTools } from './components';
|
|
5
|
+
import { pageTools } from './pages';
|
|
6
|
+
import { fail } from './result';
|
|
7
|
+
import { tokenTools } from './tokens';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* The Handoff MCP server: read-only access to the design system's components, tokens and pages.
|
|
@@ -21,10 +24,19 @@ import { registerTokenTools } from './tokens';
|
|
|
21
24
|
/** Version of the MCP surface itself, reported in `initialize`. Bump when a tool contract changes. */
|
|
22
25
|
const MCP_SERVER_VERSION = '2.0.0';
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const tools = [...componentTools, ...tokenTools, ...pageTools];
|
|
28
|
+
|
|
29
|
+
/** Compiled once per process, not per request. */
|
|
30
|
+
const validator = new AjvJsonSchemaValidator();
|
|
31
|
+
const toolsByName = new Map(
|
|
32
|
+
tools.map((tool) => [tool.definition.name, { ...tool, validate: validator.getValidator(tool.definition.inputSchema) }])
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const createMcpServer = (): Server => {
|
|
36
|
+
const server = new Server(
|
|
26
37
|
{ name: 'handoff', version: MCP_SERVER_VERSION },
|
|
27
38
|
{
|
|
39
|
+
capabilities: { tools: {} },
|
|
28
40
|
instructions:
|
|
29
41
|
'Design-system knowledge for this project. Before writing UI code, find an existing component ' +
|
|
30
42
|
'with handoff_search_components and read it with handoff_get_component. Take colors, typography ' +
|
|
@@ -33,8 +45,20 @@ export const createMcpServer = (): McpServer => {
|
|
|
33
45
|
}
|
|
34
46
|
);
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: tools.map((tool) => tool.definition) }));
|
|
49
|
+
|
|
50
|
+
// Unknown tools and invalid arguments are tool errors, so the agent can read them and retry.
|
|
51
|
+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
52
|
+
const tool = toolsByName.get(params.name);
|
|
53
|
+
if (!tool) {
|
|
54
|
+
return fail(`Tool "${params.name}" was not found.`);
|
|
55
|
+
}
|
|
56
|
+
const args = tool.validate(params.arguments ?? {});
|
|
57
|
+
if (!args.valid) {
|
|
58
|
+
return fail(`Invalid arguments for tool "${params.name}": ${args.errorMessage}.`);
|
|
59
|
+
}
|
|
60
|
+
return tool.run(args.data);
|
|
61
|
+
});
|
|
62
|
+
|
|
39
63
|
return server;
|
|
40
64
|
};
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
1
|
import type { TokenArtifactResource } from '@handoff/store';
|
|
4
2
|
import type { DocsBackend, TokenSetDetail } from '../docs-api/backend';
|
|
5
3
|
import { fail, ok, read } from './result';
|
|
4
|
+
import { defineTool, type McpTool } from './tool';
|
|
6
5
|
|
|
7
6
|
/** The token tool, and the reshaping that turns stored token sets into variables an agent can emit. */
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
type TokenFormat = 'css' | 'scss' | 'styleDictionary' | 'types';
|
|
9
|
+
|
|
10
|
+
export const tokenTools: McpTool[] = [
|
|
11
|
+
defineTool<{ set?: string; kind?: 'foundation' | 'component'; format?: TokenFormat }>(
|
|
12
12
|
{
|
|
13
|
+
name: 'handoff_get_tokens',
|
|
13
14
|
title: 'Get design tokens',
|
|
14
15
|
description:
|
|
15
16
|
'Design tokens. With no arguments: the list of token sets, plus all foundation tokens (colors, ' +
|
|
@@ -20,15 +21,20 @@ export const registerTokenTools = (server: McpServer): void => {
|
|
|
20
21
|
'stylesheet is available.',
|
|
21
22
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
22
23
|
inputSchema: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
set: { type: 'string', description: 'Token set id from the list, for example "foundation/colors".' },
|
|
27
|
+
kind: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
enum: ['foundation', 'component'],
|
|
30
|
+
description: 'Without `set`: list only sets of this kind. `component` leaves out the foundation tokens.',
|
|
31
|
+
},
|
|
32
|
+
format: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
enum: ['css', 'scss', 'styleDictionary', 'types'] satisfies TokenFormat[],
|
|
35
|
+
description: 'With `set`: return the generated file in this format.',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
32
38
|
},
|
|
33
39
|
},
|
|
34
40
|
async ({ set, kind, format }) =>
|
|
@@ -57,8 +63,8 @@ export const registerTokenTools = (server: McpServer): void => {
|
|
|
57
63
|
}
|
|
58
64
|
return ok({ id: detail.id, kind: detail.kind, format, path: artifact.path, content: artifact.content });
|
|
59
65
|
})
|
|
60
|
-
)
|
|
61
|
-
|
|
66
|
+
),
|
|
67
|
+
];
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* A token set's payload, under a key naming which of the four forms it is, so the agent never has to
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One MCP tool: its listing, with a plain JSON Schema for its input, and its handler.
|
|
5
|
+
*
|
|
6
|
+
* Plain JSON Schema keeps Zod out of the app. The Next build type-checks this code against the
|
|
7
|
+
* consumer's `node_modules`, where a second Zod copy makes the SDK's Zod types incompatible with ours.
|
|
8
|
+
*/
|
|
9
|
+
export interface McpTool {
|
|
10
|
+
definition: Tool;
|
|
11
|
+
/** Runs with arguments already validated against `definition.inputSchema`. */
|
|
12
|
+
run: (args: unknown) => Promise<CallToolResult>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `Args` must match `definition.inputSchema`; the server validates against the schema before `run`. */
|
|
16
|
+
export const defineTool = <Args>(definition: Tool, run: (args: Args) => Promise<CallToolResult>): McpTool => ({
|
|
17
|
+
definition,
|
|
18
|
+
run: run as McpTool['run'],
|
|
19
|
+
});
|
package/src/app-builder/build.ts
CHANGED
|
@@ -582,7 +582,7 @@ containers, custom Node servers, and other non-Vercel hosts.
|
|
|
582
582
|
* Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
|
|
583
583
|
* runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
|
|
584
584
|
* runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
|
|
585
|
-
*
|
|
585
|
+
* is only needed when `/api/mcp/` is enabled; the driver package is
|
|
586
586
|
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
|
|
587
587
|
* SMTP email provider needs `nodemailer`.
|
|
588
588
|
*/
|
|
@@ -592,7 +592,7 @@ const getRequiredRegistryRuntimeModules = (handoff: Handoff): string[] => {
|
|
|
592
592
|
// The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
|
|
593
593
|
// endpoint is served.
|
|
594
594
|
if (isMcpEnabled(handoff.config) || ai.enabled) {
|
|
595
|
-
base.push('@modelcontextprotocol/sdk'
|
|
595
|
+
base.push('@modelcontextprotocol/sdk');
|
|
596
596
|
}
|
|
597
597
|
if (ai.enabled) {
|
|
598
598
|
base.push('ai', '@ai-sdk/openai-compatible');
|