Skip to content

Commit f822c12

Browse files
authored
Merge pull request #632 from modelcontextprotocol/ihrpr/resource-link
Add support for resource link
2 parents ddc1a0c + 1e5f0e1 commit f822c12

File tree

5 files changed

+435
-16
lines changed

5 files changed

+435
-16
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,42 @@ server.registerTool(
192192
};
193193
}
194194
);
195+
196+
// Tool that returns ResourceLinks
197+
server.registerTool(
198+
"list-files",
199+
{
200+
title: "List Files",
201+
description: "List project files",
202+
inputSchema: { pattern: z.string() }
203+
},
204+
async ({ pattern }) => ({
205+
content: [
206+
{ type: "text", text: `Found files matching "${pattern}":` },
207+
// ResourceLinks let tools return references without file content
208+
{
209+
type: "resource_link",
210+
uri: "file:///project/README.md",
211+
name: "README.md",
212+
mimeType: "text/markdown",
213+
description: 'A README file'
214+
},
215+
{
216+
type: "resource_link",
217+
uri: "file:///project/src/index.ts",
218+
name: "index.ts",
219+
mimeType: "text/typescript",
220+
description: 'An index file'
221+
}
222+
]
223+
})
224+
);
195225
```
196226

227+
#### ResourceLinks
228+
229+
Tools can return `ResourceLink` objects to reference resources without embedding their full content. This is essential for performance when dealing with large files or many resources - clients can then selectively read only the resources they need using the provided URIs.
230+
197231
### Prompts
198232

199233
Prompts are reusable templates that help LLMs interact with your server effectively:

src/examples/client/simpleStreamableHttp.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
ListResourcesResultSchema,
1515
LoggingMessageNotificationSchema,
1616
ResourceListChangedNotificationSchema,
17+
ReadResourceRequest,
18+
ReadResourceResultSchema,
19+
ResourceLink,
1720
} from '../../types.js';
1821
import { getDisplayName } from '../../shared/metadataUtils.js';
1922

@@ -60,6 +63,7 @@ function printHelp(): void {
6063
console.log(' list-prompts - List available prompts');
6164
console.log(' get-prompt [name] [args] - Get a prompt with optional JSON arguments');
6265
console.log(' list-resources - List available resources');
66+
console.log(' read-resource <uri> - Read a specific resource by URI');
6367
console.log(' help - Show this help');
6468
console.log(' quit - Exit the program');
6569
}
@@ -155,6 +159,14 @@ function commandLoop(): void {
155159
await listResources();
156160
break;
157161

162+
case 'read-resource':
163+
if (args.length < 2) {
164+
console.log('Usage: read-resource <uri>');
165+
} else {
166+
await readResource(args[1]);
167+
}
168+
break;
169+
158170
case 'help':
159171
printHelp();
160172
break;
@@ -345,13 +357,37 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
345357
const result = await client.request(request, CallToolResultSchema);
346358

347359
console.log('Tool result:');
360+
const resourceLinks: ResourceLink[] = [];
361+
348362
result.content.forEach(item => {
349363
if (item.type === 'text') {
350364
console.log(` ${item.text}`);
365+
} else if (item.type === 'resource_link') {
366+
const resourceLink = item as ResourceLink;
367+
resourceLinks.push(resourceLink);
368+
console.log(` 📁 Resource Link: ${resourceLink.name}`);
369+
console.log(` URI: ${resourceLink.uri}`);
370+
if (resourceLink.mimeType) {
371+
console.log(` Type: ${resourceLink.mimeType}`);
372+
}
373+
if (resourceLink.description) {
374+
console.log(` Description: ${resourceLink.description}`);
375+
}
376+
} else if (item.type === 'resource') {
377+
console.log(` [Embedded Resource: ${item.resource.uri}]`);
378+
} else if (item.type === 'image') {
379+
console.log(` [Image: ${item.mimeType}]`);
380+
} else if (item.type === 'audio') {
381+
console.log(` [Audio: ${item.mimeType}]`);
351382
} else {
352-
console.log(` ${item.type} content:`, item);
383+
console.log(` [Unknown content type]:`, item);
353384
}
354385
});
386+
387+
// Offer to read resource links
388+
if (resourceLinks.length > 0) {
389+
console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
390+
}
355391
} catch (error) {
356392
console.log(`Error calling tool ${name}: ${error}`);
357393
}
@@ -489,6 +525,42 @@ async function listResources(): Promise<void> {
489525
}
490526
}
491527

528+
async function readResource(uri: string): Promise<void> {
529+
if (!client) {
530+
console.log('Not connected to server.');
531+
return;
532+
}
533+
534+
try {
535+
const request: ReadResourceRequest = {
536+
method: 'resources/read',
537+
params: { uri }
538+
};
539+
540+
console.log(`Reading resource: ${uri}`);
541+
const result = await client.request(request, ReadResourceResultSchema);
542+
543+
console.log('Resource contents:');
544+
for (const content of result.contents) {
545+
console.log(` URI: ${content.uri}`);
546+
if (content.mimeType) {
547+
console.log(` Type: ${content.mimeType}`);
548+
}
549+
550+
if ('text' in content && typeof content.text === 'string') {
551+
console.log(' Content:');
552+
console.log(' ---');
553+
console.log(content.text.split('\n').map((line: string) => ' ' + line).join('\n'));
554+
console.log(' ---');
555+
} else if ('blob' in content && typeof content.blob === 'string') {
556+
console.log(` [Binary data: ${content.blob.length} bytes]`);
557+
}
558+
}
559+
} catch (error) {
560+
console.log(`Error reading resource ${uri}: ${error}`);
561+
}
562+
}
563+
492564
async function cleanup(): Promise<void> {
493565
if (client && transport) {
494566
try {

src/examples/server/simpleStreamableHttp.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js';
55
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
66
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
77
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
8-
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult } from '../../types.js';
8+
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult, ResourceLink } from '../../types.js';
99
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
1010
import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
1111
import { OAuthMetadata } from 'src/shared/auth.js';
@@ -173,6 +173,99 @@ const getServer = () => {
173173
};
174174
}
175175
);
176+
177+
// Create additional resources for ResourceLink demonstration
178+
server.registerResource(
179+
'example-file-1',
180+
'file:///example/file1.txt',
181+
{
182+
title: 'Example File 1',
183+
description: 'First example file for ResourceLink demonstration',
184+
mimeType: 'text/plain'
185+
},
186+
async (): Promise<ReadResourceResult> => {
187+
return {
188+
contents: [
189+
{
190+
uri: 'file:///example/file1.txt',
191+
text: 'This is the content of file 1',
192+
},
193+
],
194+
};
195+
}
196+
);
197+
198+
server.registerResource(
199+
'example-file-2',
200+
'file:///example/file2.txt',
201+
{
202+
title: 'Example File 2',
203+
description: 'Second example file for ResourceLink demonstration',
204+
mimeType: 'text/plain'
205+
},
206+
async (): Promise<ReadResourceResult> => {
207+
return {
208+
contents: [
209+
{
210+
uri: 'file:///example/file2.txt',
211+
text: 'This is the content of file 2',
212+
},
213+
],
214+
};
215+
}
216+
);
217+
218+
// Register a tool that returns ResourceLinks
219+
server.registerTool(
220+
'list-files',
221+
{
222+
title: 'List Files with ResourceLinks',
223+
description: 'Returns a list of files as ResourceLinks without embedding their content',
224+
inputSchema: {
225+
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'),
226+
},
227+
},
228+
async ({ includeDescriptions = true }): Promise<CallToolResult> => {
229+
const resourceLinks: ResourceLink[] = [
230+
{
231+
type: 'resource_link',
232+
uri: 'https://example.com/greetings/default',
233+
name: 'Default Greeting',
234+
mimeType: 'text/plain',
235+
...(includeDescriptions && { description: 'A simple greeting resource' })
236+
},
237+
{
238+
type: 'resource_link',
239+
uri: 'file:///example/file1.txt',
240+
name: 'Example File 1',
241+
mimeType: 'text/plain',
242+
...(includeDescriptions && { description: 'First example file for ResourceLink demonstration' })
243+
},
244+
{
245+
type: 'resource_link',
246+
uri: 'file:///example/file2.txt',
247+
name: 'Example File 2',
248+
mimeType: 'text/plain',
249+
...(includeDescriptions && { description: 'Second example file for ResourceLink demonstration' })
250+
}
251+
];
252+
253+
return {
254+
content: [
255+
{
256+
type: 'text',
257+
text: 'Here are the available files as resource links:',
258+
},
259+
...resourceLinks,
260+
{
261+
type: 'text',
262+
text: '\nYou can read any of these resources using their URI.',
263+
}
264+
],
265+
};
266+
}
267+
);
268+
176269
return server;
177270
};
178271

0 commit comments

Comments
 (0)