-
Notifications
You must be signed in to change notification settings - Fork 8.3k
fix(sequential-thinking): Add input sanitization for numeric parameters #2812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ketema
wants to merge
6
commits into
modelcontextprotocol:main
Choose a base branch
from
ketema:fix/input-sanitization-sequential-thinking
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+275
−12
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5de32a7
fix(sequential-thinking): Add input sanitization for numeric parameters
ketema 240221c
fix: Update regex to exclude zero from numeric parameter sanitization
ketema 2c85c1d
Merge branch 'modelcontextprotocol:main' into fix/input-sanitization-…
ketema 58a9988
fix: Update schema to accept string or number for numeric parameters
ketema ca00790
[strategos] docs: Add comprehensive explanation of schema fix and thr…
ketema 28f8ec7
[strategos] Remove .npmrc from commit (keep locally)
ketema File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
# Sequential Thinking MCP Server - Schema Fix Explanation | ||
|
||
## Problem Statement | ||
|
||
The sequential thinking MCP server was experiencing errors when LLMs passed numeric parameters as strings instead of numbers: | ||
|
||
``` | ||
Error: Invalid thoughtNumber: must be a number | ||
``` | ||
|
||
This occurred even though the string values were semantically valid (e.g., `"1"`, `"2"`, `"3"`). | ||
|
||
## Root Cause Analysis | ||
|
||
The issue had **two distinct layers**: | ||
|
||
### Layer 1: Overly Strict Schema (Client-Side) | ||
The original schema defined numeric parameters as: | ||
```typescript | ||
thoughtNumber: { | ||
type: "integer", | ||
minimum: 1 | ||
} | ||
``` | ||
|
||
**Problem**: Some MCP clients perform schema validation **before** sending requests to the server. When an LLM mistakenly sent `thoughtNumber: "1"` (string) instead of `thoughtNumber: 1` (number), these clients would reject the request entirely, making the tool unusable. | ||
|
||
### Layer 2: Missing Input Sanitization (Server-Side) | ||
Even when strings reached the server (clients without strict validation), the validation code immediately rejected them: | ||
```typescript | ||
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') { | ||
throw new Error('Invalid thoughtNumber: must be a number'); | ||
} | ||
``` | ||
|
||
**Problem**: No attempt was made to coerce valid string numbers to actual numbers before validation. | ||
|
||
## The Complete Solution: Three-Layer Defense | ||
|
||
### Layer 1: Permissive Schema (Client-Side Flexibility) | ||
```typescript | ||
thoughtNumber: { | ||
oneOf: [ | ||
{ type: "integer", minimum: 1 }, | ||
{ type: "string", pattern: "^[1-9]\\d*$" } | ||
], | ||
description: "Current thought number (numeric value, e.g., 1, 2, 3)" | ||
} | ||
``` | ||
|
||
**Purpose**: | ||
- ✅ Allows LLM mistakes - If the LLM sends `"1"` instead of `1`, don't reject it | ||
- ✅ Passes client validation - Smart clients won't block the request | ||
- ✅ Documents flexibility - Shows both types are acceptable | ||
- ✅ Prevents tool unavailability - Tool remains usable even with imperfect LLM output | ||
|
||
### Layer 2: Input Sanitization (Server-Side Coercion) | ||
```typescript | ||
private sanitizeNumericParam(value: unknown): unknown { | ||
// INPUT SANITIZATION: Coerce string numbers to actual numbers | ||
// WHY: Some MCP clients may pass numeric parameters as strings | ||
// EXPECTED: Convert valid numeric strings to numbers before validation | ||
if (typeof value === 'number') { | ||
return value; // Already a number | ||
} | ||
// Regex matches positive integers starting from 1 (excludes 0) | ||
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) { | ||
const parsed = parseInt(value, 10); | ||
if (!isNaN(parsed) && parsed > 0) { | ||
return parsed; // Coerced to number | ||
} | ||
} | ||
return value; // Return as-is, let validation fail with clear error | ||
} | ||
``` | ||
|
||
**Purpose**: | ||
- ✅ Coerces valid strings to numbers - `"1"` → `1` | ||
- ✅ Handles client variations - Works regardless of client behavior | ||
- ✅ Defense in depth - Never trust client input | ||
- ✅ Graceful handling - Converts when possible, fails clearly when not | ||
|
||
### Layer 3: Strict Validation (Server-Side Enforcement) | ||
```typescript | ||
// Sanitize numeric parameters before validation | ||
if (data.thoughtNumber !== undefined) { | ||
data.thoughtNumber = this.sanitizeNumericParam(data.thoughtNumber); | ||
} | ||
|
||
// Now validate - after sanitization, we require actual numbers | ||
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') { | ||
throw new Error('Invalid thoughtNumber: must be a number'); | ||
} | ||
``` | ||
|
||
**Purpose**: | ||
- ✅ Final enforcement - After sanitization, require actual number | ||
- ✅ Clear error messages - If sanitization couldn't convert, fail explicitly | ||
- ✅ Type safety - Guarantees downstream code gets correct type | ||
|
||
## Why Both Schema AND Sanitization Are Required | ||
|
||
### Without `oneOf` (Old Schema) | ||
``` | ||
LLM sends "1" → Client schema validation (type: integer) → ❌ REJECTED | ||
Tool becomes unusable due to LLM imperfection | ||
``` | ||
|
||
### With `oneOf` But No Sanitization | ||
``` | ||
LLM sends "1" → Client schema validation (oneOf) → ✅ PASSES | ||
→ Server validation (typeof !== 'number') → ❌ REJECTED | ||
Tool fails at server level | ||
``` | ||
|
||
### With Both `oneOf` AND Sanitization (Complete Fix) | ||
``` | ||
LLM sends "1" → Client schema validation (oneOf) → ✅ PASSES | ||
→ Server sanitization → "1" becomes 1 | ||
→ Server validation → ✅ PASSES | ||
Tool works correctly! | ||
``` | ||
|
||
## Real-World Scenarios | ||
|
||
| Input | Old Schema | New Schema + Sanitization | | ||
|-------|-----------|---------------------------| | ||
| `1` (correct type) | ✅ Works | ✅ Works | | ||
| `"1"` (wrong type, valid value) | ❌ Client blocks | ✅ Client passes → Server fixes | | ||
| `"0"` (invalid value) | ❌ Client blocks | ❌ Server rejects (sanitization returns as-is) | | ||
| `"abc"` (garbage) | ❌ Client blocks | ❌ Server rejects (sanitization returns as-is) | | ||
| `-1` (negative) | ❌ Schema rejects | ❌ Schema rejects | | ||
|
||
## Key Insight: Schema as Client-Side Contract | ||
|
||
**The schema isn't just about what the server accepts - it's about preventing overly strict client-side validation from making the tool unusable when the LLM makes minor type mistakes.** | ||
|
||
According to the MCP specification: | ||
- **Servers** are responsible for validating tool inputs | ||
- **Clients SHOULD** validate (but it's optional, not required) | ||
- **The schema is primarily for documentation and LLM guidance** | ||
|
||
The MCP SDK does **not** enforce server-side schema validation. The schema tells clients what to send, but the server must still validate because clients might not respect it. | ||
|
||
## Implementation Details | ||
|
||
### Files Modified | ||
1. **index.ts** (lines 34-50): Added `sanitizeNumericParam()` method | ||
2. **index.ts** (lines 52-67): Updated `validateThoughtData()` to sanitize before validating | ||
3. **index.ts** (lines 241-272): Updated schema to use `oneOf` for all numeric parameters | ||
|
||
### Parameters Updated | ||
All numeric parameters now accept both integers and strings: | ||
- `thoughtNumber` | ||
- `totalThoughts` | ||
- `revisesThought` | ||
- `branchFromThought` | ||
|
||
### Pattern Used | ||
```typescript | ||
oneOf: [ | ||
{ type: "integer", minimum: 1 }, | ||
{ type: "string", pattern: "^[1-9]\\d*$" } | ||
] | ||
``` | ||
|
||
**Why this pattern?** | ||
- `minimum: 1` - Thought numbers are 1-indexed (1, 2, 3, ...) | ||
- `pattern: "^[1-9]\\d*$"` - Matches positive integers starting from 1, excludes 0 and negative numbers | ||
- Semantic consistency - Both schema and sanitization enforce the same rules | ||
|
||
## Design Philosophy | ||
|
||
This fix exemplifies robust API design: | ||
|
||
1. **Be liberal in what you accept** (schema: `oneOf`) | ||
2. **Be strict in what you produce** (sanitization + validation) | ||
3. **Prioritize availability** (don't let minor LLM mistakes break tools) | ||
4. **Maintain correctness** (still reject truly invalid input) | ||
|
||
The schema says "we're flexible," the sanitization says "we'll help you," and the validation says "but we still have standards." | ||
|
||
## Testing | ||
|
||
### Manual Testing | ||
```typescript | ||
// Test 1: Number (always worked) | ||
thoughtNumber: 1 // ✅ PASS | ||
|
||
// Test 2: String (now works) | ||
thoughtNumber: "2" // ✅ PASS (sanitized to 2) | ||
|
||
// Test 3: Invalid string (correctly rejected) | ||
thoughtNumber: "0" // ❌ FAIL (sanitization returns as-is, validation rejects) | ||
|
||
// Test 4: Garbage (correctly rejected) | ||
thoughtNumber: "abc" // ❌ FAIL (sanitization returns as-is, validation rejects) | ||
``` | ||
|
||
### Build Verification | ||
```bash | ||
npm run build # Compiles TypeScript to JavaScript | ||
grep -A 10 "oneOf" dist/index.js # Verify schema in built file | ||
``` | ||
|
||
## Commits | ||
|
||
1. **240221c**: Initial regex fix for thought number validation | ||
2. **5de32a7**: Added input sanitization for numeric parameters | ||
3. **58a9988**: Updated schema to accept string or number using `oneOf` | ||
|
||
## References | ||
|
||
- [MCP Tools Specification](https://modelcontextprotocol.io/docs/concepts/tools) | ||
- [JSON Schema oneOf](https://json-schema.org/understanding-json-schema/reference/combining#oneof) | ||
- Original issue: LLMs passing numeric parameters as strings | ||
|
||
## Conclusion | ||
|
||
This fix ensures the sequential thinking MCP server is robust, user-friendly, and resilient to common LLM output variations. By accepting both types in the schema and sanitizing on the server, we maintain tool availability while ensuring correctness. | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this intentionally checked in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes the explanation was included on purpose. If you do not wish it or if it violates a standard let me know and I will remove it.