Skip to content

Commit 85f3996

Browse files
feat(core): export type predicates for spec data-model types
Adds isContentBlock, isTool, isImplementation, isResourceLink, and isEmbeddedResource to the public type-guard surface alongside the existing isCallToolResult. Each predicate validates against the internal Zod schema. Lets extension authors validate spec types embedded inside custom-method payloads (e.g. a CallToolResult inside a ui/notifications/tool-result) without needing the Zod schemas themselves. Also moves guards.test.ts from src/types/ to test/types/ so vitest actually picks it up (the include pattern is test/**/*.test.ts).
1 parent 1693668 commit 85f3996

4 files changed

Lines changed: 166 additions & 4 deletions

File tree

.changeset/spec-type-predicates.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
Export type predicates (`isContentBlock`, `isTool`, `isImplementation`, `isResourceLink`, `isEmbeddedResource`) for validating spec data-model types embedded in extension payloads. Each predicate performs full structural validation against the spec schema. (`isCallToolResult` was already exported.)

packages/core/src/exports/public/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,19 @@ export {
105105
assertCompleteRequestPrompt,
106106
assertCompleteRequestResourceTemplate,
107107
isCallToolResult,
108+
isContentBlock,
109+
isEmbeddedResource,
110+
isImplementation,
108111
isInitializedNotification,
109112
isInitializeRequest,
110113
isJSONRPCErrorResponse,
111114
isJSONRPCNotification,
112115
isJSONRPCRequest,
113116
isJSONRPCResponse,
114117
isJSONRPCResultResponse,
118+
isResourceLink,
115119
isTaskAugmentedRequestParams,
120+
isTool,
116121
parseJSONRPCMessage
117122
} from '../../types/guards.js';
118123

packages/core/src/types/guards.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {
22
CallToolResultSchema,
3+
ContentBlockSchema,
4+
EmbeddedResourceSchema,
5+
ImplementationSchema,
36
InitializedNotificationSchema,
47
InitializeRequestSchema,
58
JSONRPCErrorResponseSchema,
@@ -8,13 +11,18 @@ import {
811
JSONRPCRequestSchema,
912
JSONRPCResponseSchema,
1013
JSONRPCResultResponseSchema,
11-
TaskAugmentedRequestParamsSchema
14+
ResourceLinkSchema,
15+
TaskAugmentedRequestParamsSchema,
16+
ToolSchema
1217
} from './schemas.js';
1318
import type {
1419
CallToolResult,
1520
CompleteRequest,
1621
CompleteRequestPrompt,
1722
CompleteRequestResourceTemplate,
23+
ContentBlock,
24+
EmbeddedResource,
25+
Implementation,
1826
InitializedNotification,
1927
InitializeRequest,
2028
JSONRPCErrorResponse,
@@ -23,7 +31,9 @@ import type {
2331
JSONRPCRequest,
2432
JSONRPCResponse,
2533
JSONRPCResultResponse,
26-
TaskAugmentedRequestParams
34+
ResourceLink,
35+
TaskAugmentedRequestParams,
36+
Tool
2737
} from './types.js';
2838

2939
/**
@@ -81,6 +91,46 @@ export const isCallToolResult = (value: unknown): value is CallToolResult => {
8191
return CallToolResultSchema.safeParse(value).success;
8292
};
8393

94+
/**
95+
* Checks if a value is a valid {@linkcode ContentBlock}.
96+
* @param value - The value to check.
97+
*
98+
* @returns True if the value is a valid {@linkcode ContentBlock}, false otherwise.
99+
*/
100+
export const isContentBlock = (value: unknown): value is ContentBlock => ContentBlockSchema.safeParse(value).success;
101+
102+
/**
103+
* Checks if a value is a valid {@linkcode Tool}.
104+
* @param value - The value to check.
105+
*
106+
* @returns True if the value is a valid {@linkcode Tool}, false otherwise.
107+
*/
108+
export const isTool = (value: unknown): value is Tool => ToolSchema.safeParse(value).success;
109+
110+
/**
111+
* Checks if a value is a valid {@linkcode Implementation}.
112+
* @param value - The value to check.
113+
*
114+
* @returns True if the value is a valid {@linkcode Implementation}, false otherwise.
115+
*/
116+
export const isImplementation = (value: unknown): value is Implementation => ImplementationSchema.safeParse(value).success;
117+
118+
/**
119+
* Checks if a value is a valid {@linkcode ResourceLink}.
120+
* @param value - The value to check.
121+
*
122+
* @returns True if the value is a valid {@linkcode ResourceLink}, false otherwise.
123+
*/
124+
export const isResourceLink = (value: unknown): value is ResourceLink => ResourceLinkSchema.safeParse(value).success;
125+
126+
/**
127+
* Checks if a value is a valid {@linkcode EmbeddedResource}.
128+
* @param value - The value to check.
129+
*
130+
* @returns True if the value is a valid {@linkcode EmbeddedResource}, false otherwise.
131+
*/
132+
export const isEmbeddedResource = (value: unknown): value is EmbeddedResource => EmbeddedResourceSchema.safeParse(value).success;
133+
84134
/**
85135
* Checks if a value is a valid {@linkcode TaskAugmentedRequestParams}.
86136
* @param value - The value to check.
Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { JSONRPC_VERSION } from './constants.js';
4-
import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from './guards.js';
3+
import { JSONRPC_VERSION } from '../../src/types/constants.js';
4+
import {
5+
isCallToolResult,
6+
isContentBlock,
7+
isEmbeddedResource,
8+
isImplementation,
9+
isJSONRPCErrorResponse,
10+
isJSONRPCResponse,
11+
isJSONRPCResultResponse,
12+
isResourceLink,
13+
isTool
14+
} from '../../src/types/guards.js';
515

616
describe('isJSONRPCResponse', () => {
717
it('returns true for a valid result response', () => {
@@ -121,3 +131,94 @@ describe('isCallToolResult', () => {
121131
).toBe(false);
122132
});
123133
});
134+
135+
describe('isContentBlock', () => {
136+
it('returns true for valid text content', () => {
137+
expect(isContentBlock({ type: 'text', text: 'hello' })).toBe(true);
138+
});
139+
140+
it('returns true for valid image content', () => {
141+
expect(isContentBlock({ type: 'image', data: 'aGVsbG8=', mimeType: 'image/png' })).toBe(true);
142+
});
143+
144+
it('returns false for invalid type discriminant', () => {
145+
expect(isContentBlock({ type: 'invalid', text: 'x' })).toBe(false);
146+
});
147+
148+
it('returns false for non-objects', () => {
149+
expect(isContentBlock(null)).toBe(false);
150+
expect(isContentBlock(undefined)).toBe(false);
151+
expect(isContentBlock('string')).toBe(false);
152+
});
153+
});
154+
155+
describe('isTool', () => {
156+
it('returns true for a minimal valid tool', () => {
157+
expect(isTool({ name: 'echo', inputSchema: { type: 'object' } })).toBe(true);
158+
});
159+
160+
it('returns false when required fields are missing', () => {
161+
expect(isTool({ name: 'echo' })).toBe(false);
162+
expect(isTool({ inputSchema: { type: 'object' } })).toBe(false);
163+
});
164+
165+
it('returns false for non-objects', () => {
166+
expect(isTool(null)).toBe(false);
167+
expect(isTool(undefined)).toBe(false);
168+
expect(isTool('string')).toBe(false);
169+
});
170+
});
171+
172+
describe('isImplementation', () => {
173+
it('returns true for valid implementation info', () => {
174+
expect(isImplementation({ name: 'my-app', version: '1.0.0' })).toBe(true);
175+
});
176+
177+
it('returns false when required fields are missing', () => {
178+
expect(isImplementation({ name: 'my-app' })).toBe(false);
179+
expect(isImplementation({ version: '1.0.0' })).toBe(false);
180+
});
181+
182+
it('returns false for non-objects', () => {
183+
expect(isImplementation(null)).toBe(false);
184+
expect(isImplementation(undefined)).toBe(false);
185+
expect(isImplementation('string')).toBe(false);
186+
});
187+
});
188+
189+
describe('isResourceLink', () => {
190+
it('returns true for a valid resource link', () => {
191+
expect(isResourceLink({ type: 'resource_link', uri: 'file:///x', name: 'x' })).toBe(true);
192+
});
193+
194+
it('returns false when required fields are missing', () => {
195+
expect(isResourceLink({ type: 'resource_link' })).toBe(false);
196+
});
197+
198+
it('returns false for non-objects', () => {
199+
expect(isResourceLink(null)).toBe(false);
200+
expect(isResourceLink(undefined)).toBe(false);
201+
expect(isResourceLink('string')).toBe(false);
202+
});
203+
});
204+
205+
describe('isEmbeddedResource', () => {
206+
it('returns true for a valid embedded resource', () => {
207+
expect(
208+
isEmbeddedResource({
209+
type: 'resource',
210+
resource: { uri: 'file:///x', text: 'hello' }
211+
})
212+
).toBe(true);
213+
});
214+
215+
it('returns false when resource is missing', () => {
216+
expect(isEmbeddedResource({ type: 'resource' })).toBe(false);
217+
});
218+
219+
it('returns false for non-objects', () => {
220+
expect(isEmbeddedResource(null)).toBe(false);
221+
expect(isEmbeddedResource(undefined)).toBe(false);
222+
expect(isEmbeddedResource('string')).toBe(false);
223+
});
224+
});

0 commit comments

Comments
 (0)