Skip to content

Commit 43a7a1e

Browse files
committed
add strip types
1 parent 0d54517 commit 43a7a1e

File tree

8 files changed

+1875
-3
lines changed

8 files changed

+1875
-3
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
cache: npm
2323

2424
- run: npm ci
25+
- run: npm run check:strict-types
2526
- run: npm run build
2627
- run: npm test
2728
- run: npm run lint

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ We welcome contributions to the Model Context Protocol TypeScript SDK! This docu
1414

1515
1. Create a new branch for your changes
1616
2. Make your changes
17-
3. Run `npm run lint` to ensure code style compliance
18-
4. Run `npm test` to verify all tests pass
19-
5. Submit a pull request
17+
3. If you modify `src/types.ts`, run `npm run generate:strict-types` to update strict types
18+
4. Run `npm run lint` to ensure code style compliance
19+
5. Run `npm test` to verify all tests pass
20+
6. Submit a pull request
2021

2122
## Pull Request Guidelines
2223

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,69 @@ server.registerTool("tool3", ...).disable();
950950
// Only one 'notifications/tools/list_changed' is sent.
951951
```
952952

953+
### Type Safety
954+
955+
The SDK provides type-safe definitions that validate schemas while maintaining protocol compatibility.
956+
957+
```typescript
958+
// Recommended: Use safe types that strip unknown fields
959+
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";
960+
961+
// ⚠️ Deprecated: Extensible types will be removed in a future version
962+
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
963+
```
964+
965+
**Safe types with .strip():**
966+
```typescript
967+
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";
968+
969+
// Unknown fields are automatically removed, not rejected
970+
const tool = ToolSchema.parse({
971+
name: "get-weather",
972+
description: "Get weather",
973+
inputSchema: { type: "object", properties: {} },
974+
customField: "this will be stripped" // ✓ No error, field is removed
975+
});
976+
977+
console.log(tool.customField); // undefined - field was stripped
978+
console.log(tool.name); // "get-weather" - known fields are preserved
979+
```
980+
981+
**Benefits:**
982+
- **Type safety**: Only known fields are included in results
983+
- **Protocol compatibility**: Works seamlessly with extended servers/clients
984+
- **No runtime errors**: Unknown fields are silently removed
985+
- **Forward compatibility**: Your code won't break when servers add new fields
986+
987+
**Migration Guide:**
988+
989+
If you're currently using types.js and need extensibility:
990+
1. Switch to importing from `strictTypes.js`
991+
2. Add any additional fields you need explicitly to your schemas
992+
3. For true extensibility needs, create wrapper schemas that extend the base types
993+
994+
Example migration:
995+
```typescript
996+
// Before (deprecated)
997+
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
998+
const tool = { ...baseFields, customField: "value" };
999+
1000+
// After (recommended)
1001+
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";
1002+
import { z } from "zod";
1003+
1004+
// Create your own extended schema
1005+
const ExtendedToolSchema = ToolSchema.extend({
1006+
customField: z.string()
1007+
});
1008+
const tool = ExtendedToolSchema.parse({ ...baseFields, customField: "value" });
1009+
```
1010+
1011+
Note: The following fields remain extensible for protocol compatibility:
1012+
- `experimental`: For protocol extensions
1013+
- `_meta`: For arbitrary metadata
1014+
- `properties`: For JSON Schema objects
1015+
9531016
### Low-Level Server
9541017

9551018
For more control, you can use the low-level Server class directly:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
],
3737
"scripts": {
3838
"fetch:spec-types": "curl -o spec.types.ts https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/draft/schema.ts",
39+
"generate:strict-types": "tsx scripts/generateStrictTypes.ts",
40+
"check:strict-types": "npm run generate:strict-types && git diff --exit-code src/strictTypes.ts || (echo 'Error: strictTypes.ts is out of date. Run npm run generate:strict-types' && exit 1)",
3941
"build": "npm run build:esm && npm run build:cjs",
4042
"build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json",
4143
"build:esm:w": "npm run build:esm -- -w",

scripts/generateStrictTypes.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env node
2+
import { readFileSync, writeFileSync } from 'fs';
3+
import { join, dirname } from 'path';
4+
import { fileURLToPath } from 'url';
5+
6+
const __dirname = dirname(fileURLToPath(import.meta.url));
7+
8+
// Read the original types.ts file
9+
const typesPath = join(__dirname, '../src/types.ts');
10+
const strictTypesPath = join(__dirname, '../src/strictTypes.ts');
11+
12+
let content = readFileSync(typesPath, 'utf-8');
13+
14+
// Add header comment
15+
const header = `/**
16+
* Types remove unknown
17+
* properties to maintaining compatibility with protocol extensions.
18+
*
19+
* - Protocol compatoble: Unknown fields from extended implementations are removed, not rejected
20+
* - Forward compatibility: Works with servers/clients that have additional fields
21+
*
22+
* @generated by scripts/generateStrictTypes.ts
23+
*/
24+
25+
`;
26+
27+
// Replace all .passthrough() with .strip()
28+
content = content.replace(/\.passthrough\(\)/g, '.strip()');
29+
30+
// Special handling for experimental and capabilities that should remain open
31+
// These are explicitly designed to be extensible
32+
const patternsToKeepOpen = [
33+
// Keep experimental fields open as they're meant for extensions
34+
/experimental: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
35+
// Keep _meta fields open as they're meant for arbitrary metadata
36+
/_meta: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
37+
// Keep JSON Schema properties open as they can have arbitrary fields
38+
/properties: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
39+
];
40+
41+
// Revert strip back to passthrough for these special cases
42+
patternsToKeepOpen.forEach(pattern => {
43+
content = content.replace(pattern, (match) =>
44+
match.replace('.strip()', '.passthrough()')
45+
);
46+
});
47+
48+
// Add a comment explaining the difference
49+
const explanation = `
50+
/**
51+
* Note: The following fields remain open (using .passthrough()):
52+
* - experimental: Designed for protocol extensions
53+
* - _meta: Designed for arbitrary metadata
54+
* - properties: JSON Schema properties that can have arbitrary fields
55+
*
56+
* All other objects use .strip() to remove unknown properties while
57+
* maintaining compatibility with extended protocols.
58+
*/
59+
`;
60+
61+
// Insert the explanation after the imports
62+
const importEndIndex = content.lastIndexOf('import');
63+
const importEndLineIndex = content.indexOf('\n', importEndIndex);
64+
content = content.slice(0, importEndLineIndex + 1) + explanation + content.slice(importEndLineIndex + 1);
65+
66+
// Write the strict types file
67+
writeFileSync(strictTypesPath, header + content);
68+
69+
console.log('Generated strictTypes.ts successfully!');

src/examples/strictTypesExample.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Example showing the difference between regular types and strict types
3+
*/
4+
5+
import { ToolSchema as OpenToolSchema } from "../types.js";
6+
import { ToolSchema as StrictToolSchema } from "../strictTypes.js";
7+
8+
// With regular (open) types - this is valid
9+
const openTool = OpenToolSchema.parse({
10+
name: "get-weather",
11+
description: "Get weather for a location",
12+
inputSchema: {
13+
type: "object",
14+
properties: {
15+
location: { type: "string" }
16+
}
17+
},
18+
// Extra properties are allowed
19+
customField: "This is allowed in open types",
20+
anotherExtra: 123
21+
});
22+
23+
console.log("Open tool accepts extra properties:", openTool);
24+
25+
// With strict types - this would throw an error
26+
try {
27+
StrictToolSchema.parse({
28+
name: "get-weather",
29+
description: "Get weather for a location",
30+
inputSchema: {
31+
type: "object",
32+
properties: {
33+
location: { type: "string" }
34+
}
35+
},
36+
// Extra properties cause validation to fail
37+
customField: "This is NOT allowed in strict types",
38+
anotherExtra: 123
39+
});
40+
} catch (error) {
41+
console.log("Strict tool rejects extra properties:", error instanceof Error ? error.message : String(error));
42+
}
43+
44+
// Correct usage with strict types
45+
const strictToolCorrect = StrictToolSchema.parse({
46+
name: "get-weather",
47+
description: "Get weather for a location",
48+
inputSchema: {
49+
type: "object",
50+
properties: {
51+
location: { type: "string" }
52+
}
53+
}
54+
// No extra properties
55+
});
56+
57+
console.log("Strict tool with no extra properties:", strictToolCorrect);

0 commit comments

Comments
 (0)