Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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
});
});

/***
Expand Down
4 changes: 2 additions & 2 deletions src/shared/metadataUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
9 changes: 5 additions & 4 deletions src/spec.types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,10 @@ const sdkTypeChecks = {
) => {
sdk = spec;
spec = sdk;
},
Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => {
sdk = spec;
spec = sdk;
}
};

Expand All @@ -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[] {
Expand Down
63 changes: 59 additions & 4 deletions src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,75 +95,130 @@ 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
}
};

const result = ContentBlockSchema.safeParse(embeddedResource);
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
});
}
});
});
Expand Down
50 changes: 50 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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>;
Expand Down
Loading