@tigerdata/mcp-boilerplate 0.5.0 → 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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigerdata/mcp-boilerplate",
3
- "version": "0.5.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.21.1",
40
+ "@modelcontextprotocol/sdk": "^1.22.0",
37
41
  "@opentelemetry/api": "^1.9.0",
38
- "@opentelemetry/auto-instrumentations-node": "^0.66.0",
39
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.207.0",
40
- "@opentelemetry/instrumentation-http": "^0.207.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.207.0",
46
+ "@opentelemetry/sdk-node": "^0.208.0",
43
47
  "@opentelemetry/sdk-trace-node": "^2.2.0",
44
- "@opentelemetry/semantic-conventions": "^1.37.0",
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.35.0",
51
- "@types/express": "^5.0.3",
52
- "@types/node": "^22.16.4",
53
- "ai": "^5.0.17",
54
- "eslint": "^9.35.0",
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.8.3",
57
- "typescript-eslint": "^8.43.0"
60
+ "typescript": "^5.9.3",
61
+ "typescript-eslint": "^8.46.4"
58
62
  },
59
63
  "publishConfig": {
60
64
  "access": "public"