diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 981768ec5..7dc4742e6 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2315,12 +2315,18 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { version: '1.0' }); + const mockDate = new Date().toISOString(); mcpServer.resource( 'test', 'test://resource', { description: 'Test resource', - mimeType: 'text/plain' + mimeType: 'text/plain', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }, async () => ({ contents: [ @@ -2346,6 +2352,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(result.resources).toHaveLength(1); expect(result.resources[0].description).toBe('Test resource'); expect(result.resources[0].mimeType).toBe('text/plain'); + expect(result.resources[0].annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); }); /*** diff --git a/src/shared/metadataUtils.ts b/src/shared/metadataUtils.ts index 7e9846aa8..18f84a4c9 100644 --- a/src/shared/metadataUtils.ts +++ b/src/shared/metadataUtils.ts @@ -10,14 +10,14 @@ import { BaseMetadata } from '../types.js'; * For other objects: title → name * This implements the spec requirement: "if no title is provided, name should be used for display purposes" */ -export function getDisplayName(metadata: BaseMetadata & { annotations?: { title?: string } }): string { +export function getDisplayName(metadata: BaseMetadata | (BaseMetadata & { annotations?: { title?: string } })): string { // First check for title (not undefined and not empty string) if (metadata.title !== undefined && metadata.title !== '') { return metadata.title; } // Then check for annotations.title (only present in Tool objects) - if (metadata.annotations?.title) { + if ('annotations' in metadata && metadata.annotations?.title) { return metadata.annotations.title; } diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 66e0da207..539ea38ff 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -656,6 +656,10 @@ const sdkTypeChecks = { ) => { sdk = spec; spec = sdk; + }, + Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { + sdk = spec; + spec = sdk; } }; @@ -667,10 +671,7 @@ const MISSING_SDK_TYPES = [ // These are inlined in the SDK: 'Role', 'Error', // The inner error object of a JSONRPCError - 'URLElicitationRequiredError', // In the SDK, but with a custom definition - // These aren't supported by the SDK yet: - // TODO: Add definitions to the SDK - 'Annotations' + 'URLElicitationRequiredError' // In the SDK, but with a custom definition ]; function extractExportedTypes(source: string): string[] { diff --git a/src/types.test.ts b/src/types.test.ts index 4570a443a..e0b17c628 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -95,68 +95,118 @@ describe('Types', () => { describe('ContentBlock', () => { test('should validate text content', () => { + const mockDate = new Date().toISOString(); const textContent = { type: 'text', - text: 'Hello, world!' + text: 'Hello, world!', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(textContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('text'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate image content', () => { + const mockDate = new Date().toISOString(); const imageContent = { type: 'image', data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'image/png' + mimeType: 'image/png', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(imageContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('image'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate audio content', () => { + const mockDate = new Date().toISOString(); const audioContent = { type: 'audio', data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'audio/mp3' + mimeType: 'audio/mp3', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(audioContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('audio'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate resource link content', () => { + const mockDate = new Date().toISOString(); const resourceLink = { type: 'resource_link', uri: 'file:///path/to/file.txt', name: 'file.txt', - mimeType: 'text/plain' + mimeType: 'text/plain', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: new Date().toISOString() + } }; const result = ContentBlockSchema.safeParse(resourceLink); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('resource_link'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate embedded resource content', () => { + const mockDate = new Date().toISOString(); const embeddedResource = { type: 'resource', resource: { uri: 'file:///path/to/file.txt', mimeType: 'text/plain', text: 'File contents' + }, + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate } }; @@ -164,6 +214,11 @@ describe('Types', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('resource'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); }); diff --git a/src/types.ts b/src/types.ts index 923da5447..744877db1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -790,6 +790,26 @@ export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ blob: Base64Schema }); +/** + * Optional annotations providing clients additional context about a resource. + */ +export const AnnotationsSchema = z.object({ + /** + * Intended audience(s) for the resource. + */ + audience: z.array(z.enum(['user', 'assistant'])).optional(), + + /** + * Importance hint for the resource, from 0 (least) to 1 (most). + */ + priority: z.number().min(0).max(1).optional(), + + /** + * ISO 8601 timestamp for the most recent modification. + */ + lastModified: z.iso.datetime({ offset: true }).optional() +}); + /** * A known resource that the server is capable of reading. */ @@ -813,6 +833,11 @@ export const ResourceSchema = z.object({ */ mimeType: z.optional(z.string()), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -843,6 +868,11 @@ export const ResourceTemplateSchema = z.object({ */ mimeType: z.optional(z.string()), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1035,6 +1065,11 @@ export const TextContentSchema = z.object({ */ text: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1056,6 +1091,11 @@ export const ImageContentSchema = z.object({ */ mimeType: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1077,6 +1117,11 @@ export const AudioContentSchema = z.object({ */ mimeType: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1120,6 +1165,10 @@ export const ToolUseContentSchema = z export const EmbeddedResourceSchema = z.object({ type: z.literal('resource'), resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -2219,6 +2268,7 @@ export type CancelledNotification = Infer; export type Icon = Infer; export type Icons = Infer; export type BaseMetadata = Infer; +export type Annotations = Infer; /* Initialization */ export type Implementation = Infer;