Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
cache: npm

- run: npm ci
- run: npm run check:strict-types
- run: npm run build
- run: npm test
- run: npm run lint
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ We welcome contributions to the Model Context Protocol TypeScript SDK! This docu

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

## Pull Request Guidelines

Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,69 @@ server.registerTool("tool3", ...).disable();
// Only one 'notifications/tools/list_changed' is sent.
```

### Type Safety

The SDK provides type-safe definitions that validate schemas while maintaining protocol compatibility.

```typescript
// Recommended: Use safe types that strip unknown fields
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";

// ⚠️ Deprecated: Extensible types will be removed in a future version
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
```

**Safe types with .strip():**
```typescript
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";

// Unknown fields are automatically removed, not rejected
const tool = ToolSchema.parse({
name: "get-weather",
description: "Get weather",
inputSchema: { type: "object", properties: {} },
customField: "this will be stripped" // ✓ No error, field is removed
});

console.log(tool.customField); // undefined - field was stripped
console.log(tool.name); // "get-weather" - known fields are preserved
```

**Benefits:**
- **Type safety**: Only known fields are included in results
- **Protocol compatibility**: Works seamlessly with extended servers/clients
- **No runtime errors**: Unknown fields are silently removed
- **Forward compatibility**: Your code won't break when servers add new fields

**Migration Guide:**

If you're currently using types.js and need extensibility:
1. Switch to importing from `strictTypes.js`
2. Add any additional fields you need explicitly to your schemas
3. For true extensibility needs, create wrapper schemas that extend the base types

Example migration:
```typescript
// Before (deprecated)
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
const tool = { ...baseFields, customField: "value" };

// After (recommended)
import { ToolSchema } from "@modelcontextprotocol/sdk/strictTypes.js";
import { z } from "zod";

// Create your own extended schema
const ExtendedToolSchema = ToolSchema.extend({
customField: z.string()
});
const tool = ExtendedToolSchema.parse({ ...baseFields, customField: "value" });
```

Note: The following fields remain extensible for protocol compatibility:
- `experimental`: For protocol extensions
- `_meta`: For arbitrary metadata
- `properties`: For JSON Schema objects

### Low-Level Server

For more control, you can use the low-level Server class directly:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
],
"scripts": {
"fetch:spec-types": "curl -o spec.types.ts https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/draft/schema.ts",
"generate:strict-types": "tsx scripts/generateStrictTypes.ts",
"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)",
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json",
"build:esm:w": "npm run build:esm -- -w",
Expand Down
69 changes: 69 additions & 0 deletions scripts/generateStrictTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

// Read the original types.ts file
const typesPath = join(__dirname, '../src/types.ts');
const strictTypesPath = join(__dirname, '../src/strictTypes.ts');

let content = readFileSync(typesPath, 'utf-8');

// Add header comment
const header = `/**
* Types remove unknown
* properties to maintaining compatibility with protocol extensions.
*
* - Protocol compatoble: Unknown fields from extended implementations are removed, not rejected
* - Forward compatibility: Works with servers/clients that have additional fields
*
* @generated by scripts/generateStrictTypes.ts
*/

`;

// Replace all .passthrough() with .strip()
content = content.replace(/\.passthrough\(\)/g, '.strip()');

// Special handling for experimental and capabilities that should remain open
// These are explicitly designed to be extensible
const patternsToKeepOpen = [
// Keep experimental fields open as they're meant for extensions
/experimental: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
// Keep _meta fields open as they're meant for arbitrary metadata
/_meta: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
// Keep JSON Schema properties open as they can have arbitrary fields
/properties: z\.optional\(z\.object\(\{\}\)\.strip\(\)\)/g,
];

// Revert strip back to passthrough for these special cases
patternsToKeepOpen.forEach(pattern => {
content = content.replace(pattern, (match) =>
match.replace('.strip()', '.passthrough()')
);
});

// Add a comment explaining the difference
const explanation = `
/**
* Note: The following fields remain open (using .passthrough()):
* - experimental: Designed for protocol extensions
* - _meta: Designed for arbitrary metadata
* - properties: JSON Schema properties that can have arbitrary fields
*
* All other objects use .strip() to remove unknown properties while
* maintaining compatibility with extended protocols.
*/
`;

// Insert the explanation after the imports
const importEndIndex = content.lastIndexOf('import');
const importEndLineIndex = content.indexOf('\n', importEndIndex);
content = content.slice(0, importEndLineIndex + 1) + explanation + content.slice(importEndLineIndex + 1);

// Write the strict types file
writeFileSync(strictTypesPath, header + content);

console.log('Generated strictTypes.ts successfully!');
37 changes: 37 additions & 0 deletions src/examples/strictTypesExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Example showing the difference between extensible types and safe types
*
* - Extensible types (types.js): Use .passthrough() - keep all fields
* - Safe types (strictTypes.js): Use .strip() - remove unknown fields
*/

import { ToolSchema as ExtensibleToolSchema } from "../types.js";
import { ToolSchema } from "../strictTypes.js";

const toolData = {
name: "get-weather",
description: "Get weather for a location",
inputSchema: {
type: "object",
properties: {
location: { type: "string" }
}
},
// Extra properties that aren't in the schema
customField: "This is an extension",
};

// With extensible types - ALL fields are preserved
const extensibleTool = ExtensibleToolSchema.parse(toolData);

console.log("Extensible tool keeps ALL properties:");
console.log("- name:", extensibleTool.name);
// Type assertion to access the extra field
console.log("- customField:", (extensibleTool as Record<string, unknown>).customField); // "This is an extension"

// With safe types - unknown fields are silently stripped
const safeTool = ToolSchema.parse(toolData);

console.log("\nSafe tool strips unknown properties:");
// Type assertion to check the field was removed
console.log("- customField:", (safeTool as Record<string, unknown>).customField); // undefined (stripped)
Loading