@tigerdata/mcp-boilerplate 0.4.3 → 0.6.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.
- package/README.md +23 -0
- package/dist/eslintPlugin.d.ts +13 -0
- package/dist/eslintPlugin.js +154 -0
- package/dist/http/api.js +2 -4
- package/dist/http/mcp.d.ts +2 -2
- package/dist/http/mcp.js +26 -2
- package/dist/httpServer.d.ts +3 -2
- package/dist/httpServer.js +4 -2
- package/dist/index.d.ts +2 -2
- package/dist/mcpServer.d.ts +8 -3
- package/dist/mcpServer.js +195 -84
- package/dist/stdio.d.ts +4 -3
- package/dist/stdio.js +2 -1
- package/dist/types.d.ts +41 -2
- package/package.json +18 -14
package/README.md
CHANGED
|
@@ -5,16 +5,39 @@ This provides some common code for creating a [Model Context Protocol](https://m
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
7
|
1. Clone the repository:
|
|
8
|
+
|
|
8
9
|
```bash
|
|
9
10
|
git clone <repository-url>
|
|
10
11
|
cd mcp-boilerplate-node
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
2. Install dependencies:
|
|
15
|
+
|
|
14
16
|
```bash
|
|
15
17
|
npm install
|
|
16
18
|
```
|
|
17
19
|
|
|
20
|
+
## Eslint Plugin
|
|
21
|
+
|
|
22
|
+
This project includes a custom ESLint plugin to guard against the problematic use of optional parameters for tool inputs. Doing so leads to tools that are incompatible with certain models, such as GPT-5.
|
|
23
|
+
|
|
24
|
+
Add to your `eslint.config.mjs`:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import boilerplatePlugin from '@tigerdata/mcp-boilerplate/eslintPlugin';
|
|
28
|
+
export default [
|
|
29
|
+
// ... your existing config
|
|
30
|
+
{
|
|
31
|
+
plugins: {
|
|
32
|
+
'mcp-boilerplate': boilerplatePlugin,
|
|
33
|
+
},
|
|
34
|
+
rules: {
|
|
35
|
+
'mcp-boilerplate/no-optional-tool-params': 'error',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
```
|
|
40
|
+
|
|
18
41
|
## Development
|
|
19
42
|
|
|
20
43
|
### Build
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript ESLint plugin for custom rules specific to this project
|
|
3
|
+
*/
|
|
4
|
+
import type { Rule } from 'eslint';
|
|
5
|
+
export declare const rules: {
|
|
6
|
+
'no-optional-input-schema': Rule.RuleModule;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: {
|
|
9
|
+
rules: {
|
|
10
|
+
'no-optional-input-schema': Rule.RuleModule;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript ESLint plugin for custom rules specific to this project
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Rule: no-optional-in-input-schema
|
|
6
|
+
*
|
|
7
|
+
* Detects when `.optional()`, `.default()`, or `.nullish()` are called on zod schemas
|
|
8
|
+
* that are used in the `inputSchema` property of ApiFactory config objects.
|
|
9
|
+
*
|
|
10
|
+
* Some LLMs (like GPT-5) require all tool input parameters to be marked as required
|
|
11
|
+
* in the schema, otherwise the tools become completely unusable. Using .optional(),
|
|
12
|
+
* .default(), or .nullish() makes parameters optional in the JSON schema, breaking
|
|
13
|
+
* compatibility with these LLMs.
|
|
14
|
+
*/
|
|
15
|
+
const noOptionalInputSchema = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Disallow .optional(), .default(), and .nullish() on zod schemas in ApiFactory inputSchema',
|
|
20
|
+
category: 'Best Practices',
|
|
21
|
+
recommended: true,
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
noOptional: 'Avoid using .optional(), .default(), or .nullish() on zod schemas in inputSchema. Some LLMs (like GPT-5) require all tool parameters to be marked as required, and tools become unusable otherwise. Use .nullable() instead if you need to accept null values, or handle empty/missing values in your function implementation.',
|
|
25
|
+
},
|
|
26
|
+
schema: [], // no options
|
|
27
|
+
},
|
|
28
|
+
create(context) {
|
|
29
|
+
// Track variables that are used as the Input type parameter in ApiFactory<Context, Input, Output>
|
|
30
|
+
const apiFactoryInputSchemas = new Set();
|
|
31
|
+
const problematicCalls = [];
|
|
32
|
+
return {
|
|
33
|
+
// Detect ApiFactory type annotations and extract the Input type parameter
|
|
34
|
+
VariableDeclarator(node) {
|
|
35
|
+
const varNode = node;
|
|
36
|
+
// Check if this variable has a TypeScript type annotation
|
|
37
|
+
if (varNode.id?.typeAnnotation?.typeAnnotation) {
|
|
38
|
+
const typeAnn = varNode.id.typeAnnotation.typeAnnotation;
|
|
39
|
+
// Look for ApiFactory type reference
|
|
40
|
+
if (typeAnn.type === 'TSTypeReference' &&
|
|
41
|
+
typeAnn.typeName?.name === 'ApiFactory') {
|
|
42
|
+
// Get the type parameters
|
|
43
|
+
const typeParams = typeAnn.typeArguments?.params;
|
|
44
|
+
if (typeParams && typeParams.length >= 2) {
|
|
45
|
+
const inputTypeParam = typeParams[1];
|
|
46
|
+
// Check if it's a typeof reference (e.g., typeof inputSchema2)
|
|
47
|
+
if (inputTypeParam.type === 'TSTypeQuery' &&
|
|
48
|
+
inputTypeParam.exprName?.type === 'Identifier') {
|
|
49
|
+
apiFactoryInputSchemas.add(inputTypeParam.exprName.name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
// Collect all .optional(), .default(), and .nullish() calls on zod schemas
|
|
56
|
+
CallExpression(node) {
|
|
57
|
+
const callNode = node;
|
|
58
|
+
// Check if this is a .optional(), .default(), or .nullish() call
|
|
59
|
+
if (callNode.callee.type === 'MemberExpression') {
|
|
60
|
+
const memberNode = callNode.callee;
|
|
61
|
+
if (memberNode.property.type === 'Identifier' &&
|
|
62
|
+
['optional', 'default', 'nullish'].includes(memberNode.property.name)) {
|
|
63
|
+
// Check if it's being called on a zod schema
|
|
64
|
+
const isZodSchema = isLikelyZodSchema(memberNode.object);
|
|
65
|
+
if (isZodSchema) {
|
|
66
|
+
problematicCalls.push(callNode);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
// After processing the entire file, check all problematic calls
|
|
72
|
+
'Program:exit'() {
|
|
73
|
+
for (const node of problematicCalls) {
|
|
74
|
+
if (isInsideApiFactoryInputSchema(node, context, apiFactoryInputSchemas)) {
|
|
75
|
+
const memberNode = node.callee;
|
|
76
|
+
context.report({
|
|
77
|
+
node: memberNode.property,
|
|
78
|
+
messageId: 'noOptional',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Check if a node is inside a schema that's used as an ApiFactory Input type parameter
|
|
88
|
+
*/
|
|
89
|
+
function isInsideApiFactoryInputSchema(node, context, apiFactoryInputSchemas) {
|
|
90
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.();
|
|
91
|
+
const ancestors = sourceCode?.getAncestors?.(node) ?? [];
|
|
92
|
+
// Check ancestors for variables that are ApiFactory input schemas
|
|
93
|
+
for (const ancestor of ancestors) {
|
|
94
|
+
// Check if ancestor is a VariableDeclarator whose name is in apiFactoryInputSchemas
|
|
95
|
+
if (ancestor.type === 'VariableDeclarator') {
|
|
96
|
+
const varNode = ancestor;
|
|
97
|
+
if (varNode.id?.type === 'Identifier' &&
|
|
98
|
+
apiFactoryInputSchemas.has(varNode.id.name)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Fallback: walk up parent chain if node.parent is available
|
|
104
|
+
let current = node.parent;
|
|
105
|
+
while (current) {
|
|
106
|
+
// Variable that's an ApiFactory input schema
|
|
107
|
+
if (current.type === 'VariableDeclarator') {
|
|
108
|
+
const varNode = current;
|
|
109
|
+
if (varNode.id?.type === 'Identifier' &&
|
|
110
|
+
apiFactoryInputSchemas.has(varNode.id.name)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
current = current.parent;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Heuristic to determine if a node is likely a zod schema
|
|
120
|
+
*/
|
|
121
|
+
function isLikelyZodSchema(node) {
|
|
122
|
+
if (!node || node.type === 'PrivateIdentifier')
|
|
123
|
+
return false;
|
|
124
|
+
// Direct z identifier (the base of all zod schemas)
|
|
125
|
+
if (node.type === 'Identifier') {
|
|
126
|
+
return node.name === 'z';
|
|
127
|
+
}
|
|
128
|
+
// Direct z.* calls (e.g., z.string)
|
|
129
|
+
if (node.type === 'MemberExpression') {
|
|
130
|
+
if (node.object.type === 'Identifier' && node.object.name === 'z') {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// Member expressions that might be chained zod methods (e.g., z.string)
|
|
134
|
+
return isLikelyZodSchema(node.object);
|
|
135
|
+
}
|
|
136
|
+
// Chained method calls on zod schemas (e.g., z.string().describe())
|
|
137
|
+
if (node.type === 'CallExpression') {
|
|
138
|
+
if (node.callee.type === 'MemberExpression') {
|
|
139
|
+
// Recursively check the object of the member expression
|
|
140
|
+
return isLikelyZodSchema(node.callee.object);
|
|
141
|
+
}
|
|
142
|
+
// Also check if the callee itself is 'z'
|
|
143
|
+
if (node.callee.type === 'Identifier') {
|
|
144
|
+
return node.callee.name === 'z';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
export const rules = {
|
|
150
|
+
'no-optional-input-schema': noOptionalInputSchema,
|
|
151
|
+
};
|
|
152
|
+
export default {
|
|
153
|
+
rules,
|
|
154
|
+
};
|
package/dist/http/api.js
CHANGED
|
@@ -8,7 +8,7 @@ export const apiRouterFactory = (context, apiFactories) => {
|
|
|
8
8
|
const router = Router();
|
|
9
9
|
router.use(bodyParser.json());
|
|
10
10
|
for (const factory of apiFactories) {
|
|
11
|
-
const tool = factory(context);
|
|
11
|
+
const tool = factory(context, {});
|
|
12
12
|
if (!tool.method || !tool.route)
|
|
13
13
|
continue;
|
|
14
14
|
router[tool.method](tool.route, async (req, res) => {
|
|
@@ -23,9 +23,7 @@ export const apiRouterFactory = (context, apiFactories) => {
|
|
|
23
23
|
parsedInput = Input.parse(input);
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
|
-
res
|
|
27
|
-
.status(400)
|
|
28
|
-
.json({
|
|
26
|
+
res.status(400).json({
|
|
29
27
|
error: 'zod validation failure',
|
|
30
28
|
issues: error.issues,
|
|
31
29
|
});
|
package/dist/http/mcp.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { RouterFactoryResult } from '../types.js';
|
|
3
|
-
export declare const mcpRouterFactory: <Context extends Record<string, unknown>>(context: Context, createServer: (context: Context) => {
|
|
2
|
+
import { RouterFactoryResult, McpFeatureFlags } from '../types.js';
|
|
3
|
+
export declare const mcpRouterFactory: <Context extends Record<string, unknown>>(context: Context, createServer: (context: Context, featureFlags: McpFeatureFlags) => {
|
|
4
4
|
server: McpServer;
|
|
5
5
|
}, { name, stateful, }?: {
|
|
6
6
|
name?: string;
|
package/dist/http/mcp.js
CHANGED
|
@@ -11,8 +11,29 @@ const tracer = trace.getTracer(name ? `${name}.router.mcp` : 'router.mcp');
|
|
|
11
11
|
export const mcpRouterFactory = (context, createServer, { name, stateful = true, } = {}) => {
|
|
12
12
|
const router = Router();
|
|
13
13
|
const transports = new Map();
|
|
14
|
+
const sessionFeatureFlags = new Map();
|
|
15
|
+
const toSet = (flag) => flag
|
|
16
|
+
? Array.isArray(flag)
|
|
17
|
+
? new Set(flag)
|
|
18
|
+
: typeof flag === 'string'
|
|
19
|
+
? new Set(flag.split(',').map((s) => s.trim()))
|
|
20
|
+
: null
|
|
21
|
+
: null;
|
|
22
|
+
const parseFeatureFlags = (req) => ({
|
|
23
|
+
prompts: req.query.prompts !== 'false' && req.query.prompts !== '0',
|
|
24
|
+
enabledPrompts: toSet(req.query.enabled_prompts),
|
|
25
|
+
disabledPrompts: toSet(req.query.disabled_prompts),
|
|
26
|
+
resources: req.query.resources !== 'false' && req.query.resources !== '0',
|
|
27
|
+
enabledResources: toSet(req.query.enabled_resources),
|
|
28
|
+
disabledResources: toSet(req.query.disabled_resources),
|
|
29
|
+
tools: req.query.tools !== 'false' && req.query.tools !== '0',
|
|
30
|
+
enabledTools: toSet(req.query.enabled_tools),
|
|
31
|
+
disabledTools: toSet(req.query.disabled_tools),
|
|
32
|
+
query: req.query,
|
|
33
|
+
});
|
|
14
34
|
const handleStatelessRequest = async (req, res) => {
|
|
15
|
-
const
|
|
35
|
+
const featureFlags = parseFeatureFlags(req);
|
|
36
|
+
const { server } = createServer(context, featureFlags);
|
|
16
37
|
const transport = new StreamableHTTPServerTransport({
|
|
17
38
|
sessionIdGenerator: undefined,
|
|
18
39
|
});
|
|
@@ -60,20 +81,23 @@ export const mcpRouterFactory = (context, createServer, { name, stateful = true,
|
|
|
60
81
|
});
|
|
61
82
|
return;
|
|
62
83
|
}
|
|
84
|
+
const featureFlags = parseFeatureFlags(req);
|
|
63
85
|
transport = new StreamableHTTPServerTransport({
|
|
64
86
|
sessionIdGenerator: () => randomUUID(),
|
|
65
87
|
onsessioninitialized: (sessionId) => {
|
|
66
88
|
log.info(`Session initialized with ID: ${sessionId}`);
|
|
67
89
|
transports.set(sessionId, transport);
|
|
90
|
+
sessionFeatureFlags.set(sessionId, featureFlags);
|
|
68
91
|
},
|
|
69
92
|
onsessionclosed: (sessionId) => {
|
|
70
93
|
if (sessionId && transports.has(sessionId)) {
|
|
71
94
|
log.info(`Transport closed for session ${sessionId}, removing from transports map`);
|
|
72
95
|
transports.delete(sessionId);
|
|
96
|
+
sessionFeatureFlags.delete(sessionId);
|
|
73
97
|
}
|
|
74
98
|
},
|
|
75
99
|
});
|
|
76
|
-
const { server } = createServer(context);
|
|
100
|
+
const { server } = createServer(context, featureFlags);
|
|
77
101
|
await server.connect(transport);
|
|
78
102
|
}
|
|
79
103
|
await transport.handleRequest(req, res, body);
|
package/dist/httpServer.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import express from 'express';
|
|
3
|
-
import { ApiFactory, PromptFactory } from './types.js';
|
|
3
|
+
import { ApiFactory, PromptFactory, ResourceFactory } from './types.js';
|
|
4
4
|
import { AdditionalSetupArgs } from './mcpServer.js';
|
|
5
5
|
import { Server } from 'node:http';
|
|
6
|
-
export declare const httpServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, additionalSetup, cleanupFn, stateful, }: {
|
|
6
|
+
export declare const httpServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, resourceFactories, additionalSetup, cleanupFn, stateful, }: {
|
|
7
7
|
name: string;
|
|
8
8
|
version?: string;
|
|
9
9
|
context: Context;
|
|
10
10
|
apiFactories?: readonly ApiFactory<Context, any, any>[];
|
|
11
11
|
promptFactories?: readonly PromptFactory<Context, any>[];
|
|
12
|
+
resourceFactories?: readonly ResourceFactory<Context>[];
|
|
12
13
|
additionalSetup?: (args: AdditionalSetupArgs<Context>) => void;
|
|
13
14
|
cleanupFn?: () => void | Promise<void>;
|
|
14
15
|
stateful?: boolean;
|
package/dist/httpServer.js
CHANGED
|
@@ -6,7 +6,7 @@ import { registerExitHandlers } from './registerExitHandlers.js';
|
|
|
6
6
|
import { mcpServerFactory } from './mcpServer.js';
|
|
7
7
|
import { log } from './logger.js';
|
|
8
8
|
import { StatusError } from './StatusError.js';
|
|
9
|
-
export const httpServerFactory = ({ name, version, context, apiFactories = [], promptFactories
|
|
9
|
+
export const httpServerFactory = ({ name, version, context, apiFactories = [], promptFactories, resourceFactories, additionalSetup, cleanupFn, stateful = true, }) => {
|
|
10
10
|
const cleanupFns = cleanupFn
|
|
11
11
|
? [cleanupFn]
|
|
12
12
|
: [];
|
|
@@ -14,13 +14,15 @@ export const httpServerFactory = ({ name, version, context, apiFactories = [], p
|
|
|
14
14
|
log.info('Starting HTTP server...');
|
|
15
15
|
const app = express();
|
|
16
16
|
app.enable('trust proxy');
|
|
17
|
-
const [mcpRouter, mcpCleanup] = mcpRouterFactory(context, () => mcpServerFactory({
|
|
17
|
+
const [mcpRouter, mcpCleanup] = mcpRouterFactory(context, (context, featureFlags) => mcpServerFactory({
|
|
18
18
|
name,
|
|
19
19
|
version,
|
|
20
20
|
context,
|
|
21
21
|
apiFactories,
|
|
22
22
|
promptFactories,
|
|
23
|
+
resourceFactories,
|
|
23
24
|
additionalSetup,
|
|
25
|
+
featureFlags,
|
|
24
26
|
}), { name, stateful });
|
|
25
27
|
cleanupFns.push(mcpCleanup);
|
|
26
28
|
app.use('/mcp', mcpRouter);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ export { cliEntrypoint } from './cliEntrypoint.js';
|
|
|
2
2
|
export { httpServerFactory } from './httpServer.js';
|
|
3
3
|
export { log } from './logger.js';
|
|
4
4
|
export { stdioServerFactory } from './stdio.js';
|
|
5
|
-
export {
|
|
5
|
+
export type { ApiFactory, McpFeatureFlags, PromptFactory, ResourceFactory, ParsedQs, } from './types.js';
|
|
6
6
|
export { StatusError } from './StatusError.js';
|
|
7
|
-
export {
|
|
7
|
+
export type { AdditionalSetupArgs } from './mcpServer.js';
|
|
8
8
|
export { withSpan, addAiResultToSpan } from './tracing.js';
|
|
9
9
|
export { registerExitHandlers } from './registerExitHandlers.js';
|
package/dist/mcpServer.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { ApiFactory, PromptFactory } from './types.js';
|
|
2
|
+
import { ApiFactory, PromptFactory, McpFeatureFlags, ResourceFactory } from './types.js';
|
|
3
|
+
import { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
export interface AdditionalSetupArgs<Context extends Record<string, unknown>> {
|
|
4
5
|
context: Context;
|
|
5
6
|
server: McpServer;
|
|
7
|
+
featureFlags: McpFeatureFlags;
|
|
6
8
|
}
|
|
7
|
-
export declare const mcpServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, additionalSetup, }: {
|
|
9
|
+
export declare const mcpServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, resourceFactories, additionalSetup, additionalCapabilities, featureFlags, }: {
|
|
8
10
|
name: string;
|
|
9
11
|
version?: string;
|
|
10
12
|
context: Context;
|
|
11
|
-
apiFactories
|
|
13
|
+
apiFactories?: readonly ApiFactory<Context, any, any>[];
|
|
12
14
|
promptFactories?: readonly PromptFactory<Context, any>[];
|
|
15
|
+
resourceFactories?: readonly ResourceFactory<Context>[];
|
|
13
16
|
additionalSetup?: (args: AdditionalSetupArgs<Context>) => void;
|
|
17
|
+
additionalCapabilities?: ServerCapabilities;
|
|
18
|
+
featureFlags?: McpFeatureFlags;
|
|
14
19
|
}) => {
|
|
15
20
|
server: McpServer;
|
|
16
21
|
};
|
package/dist/mcpServer.js
CHANGED
|
@@ -1,116 +1,227 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1
|
+
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { SpanStatusCode, trace, context as otelContext, propagation, SpanKind, } from '@opentelemetry/api';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
const name = process.env.OTEL_SERVICE_NAME;
|
|
5
5
|
const tracer = trace.getTracer(name ? `${name}.mcpServer` : 'mcpServer');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const toSet = (str) => str ? new Set(str.split(',').map((s) => s.trim())) : null;
|
|
7
|
+
const enabledTools = toSet(process.env.MCP_ENABLED_TOOLS);
|
|
8
|
+
const disabledTools = toSet(process.env.MCP_DISABLED_TOOLS);
|
|
9
|
+
const enabledPrompts = toSet(process.env.MCP_ENABLED_PROMPTS);
|
|
10
|
+
const disabledPrompts = toSet(process.env.MCP_DISABLED_PROMPTS);
|
|
11
|
+
const enabledResources = toSet(process.env.MCP_ENABLED_RESOURCES);
|
|
12
|
+
const disabledResources = toSet(process.env.MCP_DISABLED_RESOURCES);
|
|
13
|
+
const shouldSkip = (item, enabledSets, disabledSets) => {
|
|
14
|
+
if (item.disabled)
|
|
15
|
+
return true;
|
|
16
|
+
for (const enabledSet of enabledSets) {
|
|
17
|
+
if (enabledSet && !enabledSet.has(item.name)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const disabledSet of disabledSets) {
|
|
22
|
+
if (disabledSet && disabledSet.has(item.name)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
export const mcpServerFactory = ({ name, version = '1.0.0', context, apiFactories = [], promptFactories = [], resourceFactories = [], additionalSetup, additionalCapabilities = {}, featureFlags = {}, }) => {
|
|
29
|
+
const enablePrompts = featureFlags.prompts !== false;
|
|
30
|
+
const enableResources = featureFlags.resources !== false;
|
|
31
|
+
const enableTools = featureFlags.tools !== false;
|
|
13
32
|
const server = new McpServer({
|
|
14
33
|
name,
|
|
15
34
|
version,
|
|
16
35
|
}, {
|
|
17
36
|
capabilities: {
|
|
18
|
-
tools: {},
|
|
19
|
-
...(promptFactories.length ? { prompts: {} } : null),
|
|
37
|
+
...(enableTools && apiFactories.length ? { tools: {} } : null),
|
|
38
|
+
...(enablePrompts && promptFactories.length ? { prompts: {} } : null),
|
|
39
|
+
...(enableResources && resourceFactories.length
|
|
40
|
+
? { resources: {} }
|
|
41
|
+
: null),
|
|
42
|
+
...additionalCapabilities,
|
|
20
43
|
},
|
|
21
44
|
});
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (enableTools) {
|
|
46
|
+
for (const factory of apiFactories) {
|
|
47
|
+
const tool = factory(context, featureFlags);
|
|
48
|
+
if (shouldSkip(tool, [enabledTools, featureFlags.enabledTools], [disabledTools, featureFlags.disabledTools])) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
server.registerTool(tool.name, {
|
|
52
|
+
...tool.config,
|
|
53
|
+
annotations: {
|
|
54
|
+
...tool.config.annotations,
|
|
55
|
+
// Some clients (e.g. claude code) do not yet support the title field
|
|
56
|
+
// at the top level and instead expect it in annotations. We also
|
|
57
|
+
// don't allow setting different titles in two places as that doesn't
|
|
58
|
+
// make sense.
|
|
59
|
+
title: tool.config.title,
|
|
60
|
+
},
|
|
61
|
+
}, async (args, extra) => {
|
|
62
|
+
let traceContext = otelContext.active();
|
|
63
|
+
if (extra?._meta?.traceparent) {
|
|
64
|
+
// Some MCP clients (e.g. pydantic) pass the parent trace context
|
|
65
|
+
traceContext = propagation.extract(traceContext, {
|
|
66
|
+
traceparent: extra._meta.traceparent,
|
|
67
|
+
tracestate: extra._meta.tracestate,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return tracer.startActiveSpan(`mcp.tool.${tool.name}`, { kind: SpanKind.SERVER }, traceContext, async (span) => {
|
|
71
|
+
span.setAttribute('mcp.tool.args', JSON.stringify(args));
|
|
72
|
+
try {
|
|
73
|
+
const result = await tool.fn(args);
|
|
74
|
+
const text = JSON.stringify(result);
|
|
75
|
+
span.setAttribute('mcp.tool.responseBytes', text.length);
|
|
76
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
structuredContent: result,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
log.error('Error invoking tool:', error);
|
|
89
|
+
span.recordException(error);
|
|
90
|
+
span.setStatus({
|
|
91
|
+
code: SpanStatusCode.ERROR,
|
|
92
|
+
message: error.message,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `Error: ${error.message || 'Unknown error'}`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
isError: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
span.end();
|
|
106
|
+
}
|
|
49
107
|
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (enablePrompts) {
|
|
112
|
+
for (const factory of promptFactories) {
|
|
113
|
+
const prompt = factory(context, featureFlags);
|
|
114
|
+
if (shouldSkip(prompt, [enabledPrompts, featureFlags.enabledPrompts], [disabledPrompts, featureFlags.disabledPrompts])) {
|
|
115
|
+
continue;
|
|
50
116
|
}
|
|
51
|
-
|
|
52
|
-
span.setAttribute('mcp.
|
|
117
|
+
server.registerPrompt(prompt.name, prompt.config, async (args) => tracer.startActiveSpan(`mcp.prompt.${prompt.name}`, async (span) => {
|
|
118
|
+
span.setAttribute('mcp.prompt.args', JSON.stringify(args));
|
|
53
119
|
try {
|
|
54
|
-
const result = await
|
|
55
|
-
const text = JSON.stringify(result);
|
|
56
|
-
span.setAttribute('mcp.tool.responseBytes', text.length);
|
|
120
|
+
const result = await prompt.fn(args);
|
|
57
121
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
58
|
-
return
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: 'text',
|
|
62
|
-
text,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
structuredContent: result,
|
|
66
|
-
};
|
|
122
|
+
return result;
|
|
67
123
|
}
|
|
68
124
|
catch (error) {
|
|
69
|
-
log.error('Error invoking
|
|
125
|
+
log.error('Error invoking prompt:', error);
|
|
70
126
|
span.recordException(error);
|
|
71
127
|
span.setStatus({
|
|
72
128
|
code: SpanStatusCode.ERROR,
|
|
73
129
|
message: error.message,
|
|
74
130
|
});
|
|
75
|
-
|
|
76
|
-
content: [
|
|
77
|
-
{
|
|
78
|
-
type: 'text',
|
|
79
|
-
text: `Error: ${error.message || 'Unknown error'}`,
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
isError: true,
|
|
83
|
-
};
|
|
131
|
+
throw error;
|
|
84
132
|
}
|
|
85
133
|
finally {
|
|
86
134
|
span.end();
|
|
87
135
|
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
90
138
|
}
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const result = await prompt.fn(args);
|
|
97
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
log.error('Error invoking prompt:', error);
|
|
102
|
-
span.recordException(error);
|
|
103
|
-
span.setStatus({
|
|
104
|
-
code: SpanStatusCode.ERROR,
|
|
105
|
-
message: error.message,
|
|
106
|
-
});
|
|
107
|
-
throw error;
|
|
139
|
+
if (enableResources) {
|
|
140
|
+
for (const factory of resourceFactories) {
|
|
141
|
+
const resource = factory(context, featureFlags);
|
|
142
|
+
if (shouldSkip(resource, [enabledResources, featureFlags.enabledResources], [disabledResources, featureFlags.disabledResources])) {
|
|
143
|
+
continue;
|
|
108
144
|
}
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
switch (resource.type) {
|
|
146
|
+
case 'static': {
|
|
147
|
+
server.registerResource(resource.name, resource.uri, resource.config, async (uri, extra) => tracer.startActiveSpan(`mcp.resource.static.${resource.name}`, async (span) => {
|
|
148
|
+
span.setAttribute('mcp.resource.uri', uri.toString());
|
|
149
|
+
span.setAttribute('mcp.resource.extra', JSON.stringify(extra));
|
|
150
|
+
try {
|
|
151
|
+
const result = await resource.read(uri, extra);
|
|
152
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
log.error('Error invoking resource:', error);
|
|
157
|
+
span.recordException(error);
|
|
158
|
+
span.setStatus({
|
|
159
|
+
code: SpanStatusCode.ERROR,
|
|
160
|
+
message: error.message,
|
|
161
|
+
});
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
span.end();
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'templated': {
|
|
171
|
+
server.registerResource(resource.name, new ResourceTemplate(resource.uriTemplate, {
|
|
172
|
+
list: resource.list &&
|
|
173
|
+
((extra) => tracer.startActiveSpan(`mcp.resource.templated.${resource.name}.list`, async (span) => {
|
|
174
|
+
try {
|
|
175
|
+
const result = await resource.list(extra);
|
|
176
|
+
span.setAttribute('mcp.resource.list.uris', result.resources.map((r) => r.uri).join(', '));
|
|
177
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
log.error('Error invoking resource list:', error);
|
|
182
|
+
span.recordException(error);
|
|
183
|
+
span.setStatus({
|
|
184
|
+
code: SpanStatusCode.ERROR,
|
|
185
|
+
message: error.message,
|
|
186
|
+
});
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
span.end();
|
|
191
|
+
}
|
|
192
|
+
})),
|
|
193
|
+
complete: resource.complete,
|
|
194
|
+
}), resource.config, async (uri, variables, extra) => tracer.startActiveSpan(`mcp.resource.templated.${resource.name}`, async (span) => {
|
|
195
|
+
span.setAttribute('mcp.resource.uri', uri.toString());
|
|
196
|
+
span.setAttribute('mcp.resource.variables', JSON.stringify(variables));
|
|
197
|
+
span.setAttribute('mcp.resource.extra', JSON.stringify(extra));
|
|
198
|
+
try {
|
|
199
|
+
const result = await resource.read(uri, variables, extra);
|
|
200
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
log.error('Error invoking resource:', error);
|
|
205
|
+
span.recordException(error);
|
|
206
|
+
span.setStatus({
|
|
207
|
+
code: SpanStatusCode.ERROR,
|
|
208
|
+
message: error.message,
|
|
209
|
+
});
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
span.end();
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
default: {
|
|
219
|
+
// @ts-expect-error exhaustive check
|
|
220
|
+
throw new Error(`Unknown resource type: ${resource.type}`);
|
|
221
|
+
}
|
|
111
222
|
}
|
|
112
|
-
}
|
|
223
|
+
}
|
|
113
224
|
}
|
|
114
|
-
additionalSetup?.({ context, server });
|
|
225
|
+
additionalSetup?.({ context, server, featureFlags });
|
|
115
226
|
return { server };
|
|
116
227
|
};
|
package/dist/stdio.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ApiFactory, PromptFactory } from './types.js';
|
|
2
|
+
import { ApiFactory, PromptFactory, ResourceFactory } from './types.js';
|
|
3
3
|
import { AdditionalSetupArgs } from './mcpServer.js';
|
|
4
|
-
export declare const stdioServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, additionalSetup, cleanupFn, }: {
|
|
4
|
+
export declare const stdioServerFactory: <Context extends Record<string, unknown>>({ name, version, context, apiFactories, promptFactories, resourceFactories, additionalSetup, cleanupFn, }: {
|
|
5
5
|
name: string;
|
|
6
6
|
version?: string;
|
|
7
7
|
context: Context;
|
|
8
|
-
apiFactories
|
|
8
|
+
apiFactories?: readonly ApiFactory<Context, any, any>[];
|
|
9
9
|
promptFactories?: readonly PromptFactory<Context, any>[];
|
|
10
|
+
resourceFactories?: readonly ResourceFactory<Context>[];
|
|
10
11
|
additionalSetup?: (args: AdditionalSetupArgs<Context>) => void;
|
|
11
12
|
cleanupFn?: () => Promise<void>;
|
|
12
13
|
}) => Promise<void>;
|
package/dist/stdio.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { mcpServerFactory } from './mcpServer.js';
|
|
4
4
|
import { registerExitHandlers } from './registerExitHandlers.js';
|
|
5
|
-
export const stdioServerFactory = async ({ name, version, context, apiFactories, promptFactories
|
|
5
|
+
export const stdioServerFactory = async ({ name, version, context, apiFactories, promptFactories, resourceFactories, additionalSetup, cleanupFn, }) => {
|
|
6
6
|
try {
|
|
7
7
|
console.error('Starting default (STDIO) server...');
|
|
8
8
|
const transport = new StdioServerTransport();
|
|
@@ -12,6 +12,7 @@ export const stdioServerFactory = async ({ name, version, context, apiFactories,
|
|
|
12
12
|
context,
|
|
13
13
|
apiFactories,
|
|
14
14
|
promptFactories,
|
|
15
|
+
resourceFactories,
|
|
15
16
|
additionalSetup,
|
|
16
17
|
});
|
|
17
18
|
await server.connect(transport);
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { ZodRawShape, ZodTypeAny } from 'zod';
|
|
3
3
|
import type { ToolAnnotations, GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { Router } from 'express';
|
|
5
|
+
import { CompleteResourceTemplateCallback, ListResourcesCallback, ReadResourceCallback, ReadResourceTemplateCallback, ResourceMetadata } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
6
|
export type ToolConfig<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape> = {
|
|
6
7
|
title?: string;
|
|
7
8
|
description?: string;
|
|
@@ -18,7 +19,7 @@ export interface ApiDefinition<InputArgs extends ZodRawShape, OutputArgs extends
|
|
|
18
19
|
fn: (args: z.objectOutputType<InputArgs, ZodTypeAny>) => Promise<z.objectOutputType<OutputArgs, ZodTypeAny>>;
|
|
19
20
|
pickResult?: (result: z.objectOutputType<OutputArgs, ZodTypeAny>) => SimplifiedOutputArgs;
|
|
20
21
|
}
|
|
21
|
-
export type ApiFactory<Context extends Record<string, unknown>, Input extends ZodRawShape, Output extends ZodRawShape, RestOutput = Output> = (ctx: Context) => ApiDefinition<Input, Output, RestOutput>;
|
|
22
|
+
export type ApiFactory<Context extends Record<string, unknown>, Input extends ZodRawShape, Output extends ZodRawShape, RestOutput = Output> = (ctx: Context, featureFlags: McpFeatureFlags) => ApiDefinition<Input, Output, RestOutput>;
|
|
22
23
|
export type RouterFactoryResult = [Router, () => void | Promise<void>];
|
|
23
24
|
export type PromptConfig<InputArgs extends ZodRawShape> = {
|
|
24
25
|
title?: string;
|
|
@@ -28,6 +29,44 @@ export type PromptConfig<InputArgs extends ZodRawShape> = {
|
|
|
28
29
|
export interface PromptDefinition<InputArgs extends ZodRawShape> {
|
|
29
30
|
name: string;
|
|
30
31
|
config: PromptConfig<InputArgs>;
|
|
32
|
+
disabled?: boolean;
|
|
31
33
|
fn: (args: z.objectOutputType<InputArgs, ZodTypeAny>) => Promise<GetPromptResult>;
|
|
32
34
|
}
|
|
33
|
-
export type PromptFactory<Context extends Record<string, unknown>, Input extends ZodRawShape> = (ctx: Context) => PromptDefinition<Input>;
|
|
35
|
+
export type PromptFactory<Context extends Record<string, unknown>, Input extends ZodRawShape> = (ctx: Context, featureFlags: McpFeatureFlags) => PromptDefinition<Input>;
|
|
36
|
+
export interface TemplatedResourceDefinition {
|
|
37
|
+
type: 'templated';
|
|
38
|
+
name: string;
|
|
39
|
+
uriTemplate: string;
|
|
40
|
+
list?: ListResourcesCallback;
|
|
41
|
+
complete?: {
|
|
42
|
+
[variable: string]: CompleteResourceTemplateCallback;
|
|
43
|
+
};
|
|
44
|
+
config: ResourceMetadata;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
read: ReadResourceTemplateCallback;
|
|
47
|
+
}
|
|
48
|
+
export interface StaticResourceDefinition {
|
|
49
|
+
type: 'static';
|
|
50
|
+
name: string;
|
|
51
|
+
uri: string;
|
|
52
|
+
config: ResourceMetadata;
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
read: ReadResourceCallback;
|
|
55
|
+
}
|
|
56
|
+
export type ResourceDefinition = TemplatedResourceDefinition | StaticResourceDefinition;
|
|
57
|
+
export type ResourceFactory<Context extends Record<string, unknown>> = (ctx: Context, featureFlags: McpFeatureFlags) => ResourceDefinition;
|
|
58
|
+
export interface ParsedQs {
|
|
59
|
+
[key: string]: undefined | string | ParsedQs | (string | ParsedQs)[];
|
|
60
|
+
}
|
|
61
|
+
export interface McpFeatureFlags {
|
|
62
|
+
prompts?: boolean;
|
|
63
|
+
enabledPrompts?: Set<string> | null;
|
|
64
|
+
disabledPrompts?: Set<string> | null;
|
|
65
|
+
resources?: boolean;
|
|
66
|
+
enabledResources?: Set<string> | null;
|
|
67
|
+
disabledResources?: Set<string> | null;
|
|
68
|
+
tools?: boolean;
|
|
69
|
+
enabledTools?: Set<string> | null;
|
|
70
|
+
disabledTools?: Set<string> | null;
|
|
71
|
+
query?: ParsedQs;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tigerdata/mcp-boilerplate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP boilerplate code for Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "TigerData",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"import": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts"
|
|
19
19
|
},
|
|
20
|
+
"./eslintPlugin": {
|
|
21
|
+
"import": "./dist/eslintPlugin.js",
|
|
22
|
+
"types": "./dist/eslintPlugin.d.ts"
|
|
23
|
+
},
|
|
20
24
|
"./instrumentation": {
|
|
21
25
|
"import": "./dist/instrumentation.js",
|
|
22
26
|
"types": "./dist/instrumentation.d.ts"
|
|
@@ -33,28 +37,28 @@
|
|
|
33
37
|
"lint:fix": "eslint --fix"
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
37
41
|
"@opentelemetry/api": "^1.9.0",
|
|
38
|
-
"@opentelemetry/auto-instrumentations-node": "^0.
|
|
39
|
-
"@opentelemetry/exporter-trace-otlp-grpc": "^0.
|
|
40
|
-
"@opentelemetry/instrumentation-http": "^0.
|
|
42
|
+
"@opentelemetry/auto-instrumentations-node": "^0.67.0",
|
|
43
|
+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.208.0",
|
|
44
|
+
"@opentelemetry/instrumentation-http": "^0.208.0",
|
|
41
45
|
"@opentelemetry/sdk-metrics": "^2.2.0",
|
|
42
|
-
"@opentelemetry/sdk-node": "^0.
|
|
46
|
+
"@opentelemetry/sdk-node": "^0.208.0",
|
|
43
47
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
|
44
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
48
|
+
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
45
49
|
"express": "^5.1.0",
|
|
46
50
|
"raw-body": "^3.0.1",
|
|
47
51
|
"zod": "^3.23.8"
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^9.
|
|
51
|
-
"@types/express": "^5.0.
|
|
52
|
-
"@types/node": "^22.
|
|
53
|
-
"ai": "^5.0.
|
|
54
|
-
"eslint": "^9.
|
|
54
|
+
"@eslint/js": "^9.39.1",
|
|
55
|
+
"@types/express": "^5.0.5",
|
|
56
|
+
"@types/node": "^22.19.1",
|
|
57
|
+
"ai": "^5.0.93",
|
|
58
|
+
"eslint": "^9.39.1",
|
|
55
59
|
"prettier": "^3.6.2",
|
|
56
|
-
"typescript": "^5.
|
|
57
|
-
"typescript-eslint": "^8.
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"typescript-eslint": "^8.46.4"
|
|
58
62
|
},
|
|
59
63
|
"publishConfig": {
|
|
60
64
|
"access": "public"
|