@tigerdata/mcp-boilerplate 0.5.0 → 0.7.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/index.d.ts +1 -0
- package/dist/types.d.ts +3 -0
- package/package.json +21 -15
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/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { StatusError } from './StatusError.js';
|
|
|
7
7
|
export type { AdditionalSetupArgs } from './mcpServer.js';
|
|
8
8
|
export { withSpan, addAiResultToSpan } from './tracing.js';
|
|
9
9
|
export { registerExitHandlers } from './registerExitHandlers.js';
|
|
10
|
+
export type { InferSchema } from './types.js';
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tigerdata/mcp-boilerplate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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"
|
|
@@ -30,31 +34,33 @@
|
|
|
30
34
|
"prepare": "npm run build",
|
|
31
35
|
"watch": "tsc --watch",
|
|
32
36
|
"lint": "eslint",
|
|
33
|
-
"lint:fix": "eslint --fix"
|
|
37
|
+
"lint:fix": "eslint --fix",
|
|
38
|
+
"prettier:write": "prettier --write .",
|
|
39
|
+
"prettier:check": "prettier --check ."
|
|
34
40
|
},
|
|
35
41
|
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
37
43
|
"@opentelemetry/api": "^1.9.0",
|
|
38
|
-
"@opentelemetry/auto-instrumentations-node": "^0.
|
|
39
|
-
"@opentelemetry/exporter-trace-otlp-grpc": "^0.
|
|
40
|
-
"@opentelemetry/instrumentation-http": "^0.
|
|
44
|
+
"@opentelemetry/auto-instrumentations-node": "^0.67.0",
|
|
45
|
+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.208.0",
|
|
46
|
+
"@opentelemetry/instrumentation-http": "^0.208.0",
|
|
41
47
|
"@opentelemetry/sdk-metrics": "^2.2.0",
|
|
42
|
-
"@opentelemetry/sdk-node": "^0.
|
|
48
|
+
"@opentelemetry/sdk-node": "^0.208.0",
|
|
43
49
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
|
44
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
50
|
+
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
45
51
|
"express": "^5.1.0",
|
|
46
52
|
"raw-body": "^3.0.1",
|
|
47
53
|
"zod": "^3.23.8"
|
|
48
54
|
},
|
|
49
55
|
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^9.
|
|
51
|
-
"@types/express": "^5.0.
|
|
52
|
-
"@types/node": "^22.
|
|
53
|
-
"ai": "^5.0.
|
|
54
|
-
"eslint": "^9.
|
|
56
|
+
"@eslint/js": "^9.39.1",
|
|
57
|
+
"@types/express": "^5.0.5",
|
|
58
|
+
"@types/node": "^22.19.1",
|
|
59
|
+
"ai": "^5.0.93",
|
|
60
|
+
"eslint": "^9.39.1",
|
|
55
61
|
"prettier": "^3.6.2",
|
|
56
|
-
"typescript": "^5.
|
|
57
|
-
"typescript-eslint": "^8.
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"typescript-eslint": "^8.46.4"
|
|
58
64
|
},
|
|
59
65
|
"publishConfig": {
|
|
60
66
|
"access": "public"
|