Skip to content

Commit 7e59081

Browse files
committed
add resource link
1 parent 4d03b24 commit 7e59081

File tree

5 files changed

+433
-16
lines changed

5 files changed

+433
-16
lines changed

README.md

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

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+
199231
### Prompts
200232

201233
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';
@@ -174,6 +174,99 @@ const getServer = () => {
174174
};
175175
}
176176
);
177+
178+
// Create additional resources for ResourceLink demonstration
179+
server.registerResource(
180+
'example-file-1',
181+
'file:///example/file1.txt',
182+
{
183+
title: 'Example File 1',
184+
description: 'First example file for ResourceLink demonstration',
185+
mimeType: 'text/plain'
186+
},
187+
async (): Promise<ReadResourceResult> => {
188+
return {
189+
contents: [
190+
{
191+
uri: 'file:///example/file1.txt',
192+
text: 'This is the content of file 1',
193+
},
194+
],
195+
};
196+
}
197+
);
198+
199+
server.registerResource(
200+
'example-file-2',
201+
'file:///example/file2.txt',
202+
{
203+
title: 'Example File 2',
204+
description: 'Second example file for ResourceLink demonstration',
205+
mimeType: 'text/plain'
206+
},
207+
async (): Promise<ReadResourceResult> => {
208+
return {
209+
contents: [
210+
{
211+
uri: 'file:///example/file2.txt',
212+
text: 'This is the content of file 2',
213+
},
214+
],
215+
};
216+
}
217+
);
218+
219+
// Register a tool that returns ResourceLinks
220+
server.registerTool(
221+
'list-files',
222+
{
223+
title: 'List Files with ResourceLinks',
224+
description: 'Returns a list of files as ResourceLinks without embedding their content',
225+
inputSchema: {
226+
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'),
227+
},
228+
},
229+
async ({ includeDescriptions = true }): Promise<CallToolResult> => {
230+
const resourceLinks: ResourceLink[] = [
231+
{
232+
type: 'resource_link',
233+
uri: 'https://example.com/greetings/default',
234+
name: 'Default Greeting',
235+
mimeType: 'text/plain',
236+
...(includeDescriptions && { description: 'A simple greeting resource' })
237+
},
238+
{
239+
type: 'resource_link',
240+
uri: 'file:///example/file1.txt',
241+
name: 'Example File 1',
242+
mimeType: 'text/plain',
243+
...(includeDescriptions && { description: 'First example file for ResourceLink demonstration' })
244+
},
245+
{
246+
type: 'resource_link',
247+
uri: 'file:///example/file2.txt',
248+
name: 'Example File 2',
249+
mimeType: 'text/plain',
250+
...(includeDescriptions && { description: 'Second example file for ResourceLink demonstration' })
251+
}
252+
];
253+
254+
return {
255+
content: [
256+
{
257+
type: 'text',
258+
text: 'Here are the available files as resource links:',
259+
},
260+
...resourceLinks,
261+
{
262+
type: 'text',
263+
text: '\nYou can read any of these resources using their URI.',
264+
}
265+
],
266+
};
267+
}
268+
);
269+
177270
return server;
178271
};
179272

0 commit comments

Comments
 (0)