feat: add optional resource annotations (#954)

Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
This commit is contained in:
Vincent Horvath 2025-12-03 00:22:20 -05:00 • committed by GitHub
commit 4b5c25d592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 11 deletions

View file

@ -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
});
});
/***

View file

@ -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;
}

View file

@ -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[] {

View file

@ -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
});
}
});
});

View file

@ -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<typeof CancelledNotificationSchema>;
export type Icon = Infer<typeof IconSchema>;
export type Icons = Infer<typeof IconsSchema>;
export type BaseMetadata = Infer<typeof BaseMetadataSchema>;
export type Annotations = Infer<typeof AnnotationsSchema>;
/* Initialization */
export type Implementation = Infer<typeof ImplementationSchema>;