feat: add optional resource annotations (#954)
Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
This commit is contained in:
parent
6dd7cd4e16
commit
4b5c25d592
5 changed files with 128 additions and 11 deletions
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
/***
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
50
src/types.ts
50
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<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>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue