Skip to content

Commit 4b5c25d

Browse files
feat: add optional resource annotations (#954)
Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
1 parent 6dd7cd4 commit 4b5c25d

File tree

5 files changed

+128
-11
lines changed

5 files changed

+128
-11
lines changed

src/server/mcp.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2315,12 +2315,18 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
23152315
version: '1.0'
23162316
});
23172317

2318+
const mockDate = new Date().toISOString();
23182319
mcpServer.resource(
23192320
'test',
23202321
'test://resource',
23212322
{
23222323
description: 'Test resource',
2323-
mimeType: 'text/plain'
2324+
mimeType: 'text/plain',
2325+
annotations: {
2326+
audience: ['user'],
2327+
priority: 0.5,
2328+
lastModified: mockDate
2329+
}
23242330
},
23252331
async () => ({
23262332
contents: [
@@ -2346,6 +2352,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
23462352
expect(result.resources).toHaveLength(1);
23472353
expect(result.resources[0].description).toBe('Test resource');
23482354
expect(result.resources[0].mimeType).toBe('text/plain');
2355+
expect(result.resources[0].annotations).toEqual({
2356+
audience: ['user'],
2357+
priority: 0.5,
2358+
lastModified: mockDate
2359+
});
23492360
});
23502361

23512362
/***

src/shared/metadataUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { BaseMetadata } from '../types.js';
1010
* For other objects: title → name
1111
* This implements the spec requirement: "if no title is provided, name should be used for display purposes"
1212
*/
13-
export function getDisplayName(metadata: BaseMetadata & { annotations?: { title?: string } }): string {
13+
export function getDisplayName(metadata: BaseMetadata | (BaseMetadata & { annotations?: { title?: string } })): string {
1414
// First check for title (not undefined and not empty string)
1515
if (metadata.title !== undefined && metadata.title !== '') {
1616
return metadata.title;
1717
}
1818

1919
// Then check for annotations.title (only present in Tool objects)
20-
if (metadata.annotations?.title) {
20+
if ('annotations' in metadata && metadata.annotations?.title) {
2121
return metadata.annotations.title;
2222
}
2323

src/spec.types.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,10 @@ const sdkTypeChecks = {
656656
) => {
657657
sdk = spec;
658658
spec = sdk;
659+
},
660+
Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => {
661+
sdk = spec;
662+
spec = sdk;
659663
}
660664
};
661665

@@ -667,10 +671,7 @@ const MISSING_SDK_TYPES = [
667671
// These are inlined in the SDK:
668672
'Role',
669673
'Error', // The inner error object of a JSONRPCError
670-
'URLElicitationRequiredError', // In the SDK, but with a custom definition
671-
// These aren't supported by the SDK yet:
672-
// TODO: Add definitions to the SDK
673-
'Annotations'
674+
'URLElicitationRequiredError' // In the SDK, but with a custom definition
674675
];
675676

676677
function extractExportedTypes(source: string): string[] {

src/types.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,75 +95,130 @@ describe('Types', () => {
9595

9696
describe('ContentBlock', () => {
9797
test('should validate text content', () => {
98+
const mockDate = new Date().toISOString();
9899
const textContent = {
99100
type: 'text',
100-
text: 'Hello, world!'
101+
text: 'Hello, world!',
102+
annotations: {
103+
audience: ['user'],
104+
priority: 0.5,
105+
lastModified: mockDate
106+
}
101107
};
102108

103109
const result = ContentBlockSchema.safeParse(textContent);
104110
expect(result.success).toBe(true);
105111
if (result.success) {
106112
expect(result.data.type).toBe('text');
113+
expect(result.data.annotations).toEqual({
114+
audience: ['user'],
115+
priority: 0.5,
116+
lastModified: mockDate
117+
});
107118
}
108119
});
109120

110121
test('should validate image content', () => {
122+
const mockDate = new Date().toISOString();
111123
const imageContent = {
112124
type: 'image',
113125
data: 'aGVsbG8=', // base64 encoded "hello"
114-
mimeType: 'image/png'
126+
mimeType: 'image/png',
127+
annotations: {
128+
audience: ['user'],
129+
priority: 0.5,
130+
lastModified: mockDate
131+
}
115132
};
116133

117134
const result = ContentBlockSchema.safeParse(imageContent);
118135
expect(result.success).toBe(true);
119136
if (result.success) {
120137
expect(result.data.type).toBe('image');
138+
expect(result.data.annotations).toEqual({
139+
audience: ['user'],
140+
priority: 0.5,
141+
lastModified: mockDate
142+
});
121143
}
122144
});
123145

124146
test('should validate audio content', () => {
147+
const mockDate = new Date().toISOString();
125148
const audioContent = {
126149
type: 'audio',
127150
data: 'aGVsbG8=', // base64 encoded "hello"
128-
mimeType: 'audio/mp3'
151+
mimeType: 'audio/mp3',
152+
annotations: {
153+
audience: ['user'],
154+
priority: 0.5,
155+
lastModified: mockDate
156+
}
129157
};
130158

131159
const result = ContentBlockSchema.safeParse(audioContent);
132160
expect(result.success).toBe(true);
133161
if (result.success) {
134162
expect(result.data.type).toBe('audio');
163+
expect(result.data.annotations).toEqual({
164+
audience: ['user'],
165+
priority: 0.5,
166+
lastModified: mockDate
167+
});
135168
}
136169
});
137170

138171
test('should validate resource link content', () => {
172+
const mockDate = new Date().toISOString();
139173
const resourceLink = {
140174
type: 'resource_link',
141175
uri: 'file:///path/to/file.txt',
142176
name: 'file.txt',
143-
mimeType: 'text/plain'
177+
mimeType: 'text/plain',
178+
annotations: {
179+
audience: ['user'],
180+
priority: 0.5,
181+
lastModified: new Date().toISOString()
182+
}
144183
};
145184

146185
const result = ContentBlockSchema.safeParse(resourceLink);
147186
expect(result.success).toBe(true);
148187
if (result.success) {
149188
expect(result.data.type).toBe('resource_link');
189+
expect(result.data.annotations).toEqual({
190+
audience: ['user'],
191+
priority: 0.5,
192+
lastModified: mockDate
193+
});
150194
}
151195
});
152196

153197
test('should validate embedded resource content', () => {
198+
const mockDate = new Date().toISOString();
154199
const embeddedResource = {
155200
type: 'resource',
156201
resource: {
157202
uri: 'file:///path/to/file.txt',
158203
mimeType: 'text/plain',
159204
text: 'File contents'
205+
},
206+
annotations: {
207+
audience: ['user'],
208+
priority: 0.5,
209+
lastModified: mockDate
160210
}
161211
};
162212

163213
const result = ContentBlockSchema.safeParse(embeddedResource);
164214
expect(result.success).toBe(true);
165215
if (result.success) {
166216
expect(result.data.type).toBe('resource');
217+
expect(result.data.annotations).toEqual({
218+
audience: ['user'],
219+
priority: 0.5,
220+
lastModified: mockDate
221+
});
167222
}
168223
});
169224
});

src/types.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,26 @@ export const BlobResourceContentsSchema = ResourceContentsSchema.extend({
790790
blob: Base64Schema
791791
});
792792

793+
/**
794+
* Optional annotations providing clients additional context about a resource.
795+
*/
796+
export const AnnotationsSchema = z.object({
797+
/**
798+
* Intended audience(s) for the resource.
799+
*/
800+
audience: z.array(z.enum(['user', 'assistant'])).optional(),
801+
802+
/**
803+
* Importance hint for the resource, from 0 (least) to 1 (most).
804+
*/
805+
priority: z.number().min(0).max(1).optional(),
806+
807+
/**
808+
* ISO 8601 timestamp for the most recent modification.
809+
*/
810+
lastModified: z.iso.datetime({ offset: true }).optional()
811+
});
812+
793813
/**
794814
* A known resource that the server is capable of reading.
795815
*/
@@ -813,6 +833,11 @@ export const ResourceSchema = z.object({
813833
*/
814834
mimeType: z.optional(z.string()),
815835

836+
/**
837+
* Optional annotations for the client.
838+
*/
839+
annotations: AnnotationsSchema.optional(),
840+
816841
/**
817842
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
818843
* for notes on _meta usage.
@@ -843,6 +868,11 @@ export const ResourceTemplateSchema = z.object({
843868
*/
844869
mimeType: z.optional(z.string()),
845870

871+
/**
872+
* Optional annotations for the client.
873+
*/
874+
annotations: AnnotationsSchema.optional(),
875+
846876
/**
847877
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
848878
* for notes on _meta usage.
@@ -1035,6 +1065,11 @@ export const TextContentSchema = z.object({
10351065
*/
10361066
text: z.string(),
10371067

1068+
/**
1069+
* Optional annotations for the client.
1070+
*/
1071+
annotations: AnnotationsSchema.optional(),
1072+
10381073
/**
10391074
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
10401075
* for notes on _meta usage.
@@ -1056,6 +1091,11 @@ export const ImageContentSchema = z.object({
10561091
*/
10571092
mimeType: z.string(),
10581093

1094+
/**
1095+
* Optional annotations for the client.
1096+
*/
1097+
annotations: AnnotationsSchema.optional(),
1098+
10591099
/**
10601100
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
10611101
* for notes on _meta usage.
@@ -1077,6 +1117,11 @@ export const AudioContentSchema = z.object({
10771117
*/
10781118
mimeType: z.string(),
10791119

1120+
/**
1121+
* Optional annotations for the client.
1122+
*/
1123+
annotations: AnnotationsSchema.optional(),
1124+
10801125
/**
10811126
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
10821127
* for notes on _meta usage.
@@ -1120,6 +1165,10 @@ export const ToolUseContentSchema = z
11201165
export const EmbeddedResourceSchema = z.object({
11211166
type: z.literal('resource'),
11221167
resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]),
1168+
/**
1169+
* Optional annotations for the client.
1170+
*/
1171+
annotations: AnnotationsSchema.optional(),
11231172
/**
11241173
* See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
11251174
* for notes on _meta usage.
@@ -2219,6 +2268,7 @@ export type CancelledNotification = Infer<typeof CancelledNotificationSchema>;
22192268
export type Icon = Infer<typeof IconSchema>;
22202269
export type Icons = Infer<typeof IconsSchema>;
22212270
export type BaseMetadata = Infer<typeof BaseMetadataSchema>;
2271+
export type Annotations = Infer<typeof AnnotationsSchema>;
22222272

22232273
/* Initialization */
22242274
export type Implementation = Infer<typeof ImplementationSchema>;

0 commit comments

Comments
 (0)