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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,42 @@ server.registerTool(
};
}
);

// Tool that returns ResourceLinks
server.registerTool(
"list-files",
{
title: "List Files",
description: "List project files",
inputSchema: { pattern: z.string() }
},
async ({ pattern }) => ({
content: [
{ type: "text", text: `Found files matching "${pattern}":` },
// ResourceLinks let tools return references without file content
{
type: "resource_link",
uri: "file:///project/README.md",
name: "README.md",
mimeType: "text/markdown",
description: 'A README file'
},
{
type: "resource_link",
uri: "file:///project/src/index.ts",
name: "index.ts",
mimeType: "text/typescript",
description: 'An index file'
}
]
})
);
```

#### ResourceLinks

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.

### Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:
Expand Down
74 changes: 73 additions & 1 deletion src/examples/client/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
ListResourcesResultSchema,
LoggingMessageNotificationSchema,
ResourceListChangedNotificationSchema,
ReadResourceRequest,
ReadResourceResultSchema,
ResourceLink,
} from '../../types.js';
import { getDisplayName } from '../../shared/metadataUtils.js';

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

case 'read-resource':
if (args.length < 2) {
console.log('Usage: read-resource <uri>');
} else {
await readResource(args[1]);
}
break;

case 'help':
printHelp();
break;
Expand Down Expand Up @@ -345,13 +357,37 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
const result = await client.request(request, CallToolResultSchema);

console.log('Tool result:');
const resourceLinks: ResourceLink[] = [];

result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else if (item.type === 'resource_link') {
const resourceLink = item as ResourceLink;
resourceLinks.push(resourceLink);
console.log(` 📁 Resource Link: ${resourceLink.name}`);
console.log(` URI: ${resourceLink.uri}`);
if (resourceLink.mimeType) {
console.log(` Type: ${resourceLink.mimeType}`);
}
if (resourceLink.description) {
console.log(` Description: ${resourceLink.description}`);
}
} else if (item.type === 'resource') {
console.log(` [Embedded Resource: ${item.resource.uri}]`);
} else if (item.type === 'image') {
console.log(` [Image: ${item.mimeType}]`);
} else if (item.type === 'audio') {
console.log(` [Audio: ${item.mimeType}]`);
} else {
console.log(` ${item.type} content:`, item);
console.log(` [Unknown content type]:`, item);
}
});

// Offer to read resource links
if (resourceLinks.length > 0) {
console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
}
} catch (error) {
console.log(`Error calling tool ${name}: ${error}`);
}
Expand Down Expand Up @@ -489,6 +525,42 @@ async function listResources(): Promise<void> {
}
}

async function readResource(uri: string): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}

try {
const request: ReadResourceRequest = {
method: 'resources/read',
params: { uri }
};

console.log(`Reading resource: ${uri}`);
const result = await client.request(request, ReadResourceResultSchema);

console.log('Resource contents:');
for (const content of result.contents) {
console.log(` URI: ${content.uri}`);
if (content.mimeType) {
console.log(` Type: ${content.mimeType}`);
}

if ('text' in content && typeof content.text === 'string') {
console.log(' Content:');
console.log(' ---');
console.log(content.text.split('\n').map((line: string) => ' ' + line).join('\n'));
console.log(' ---');
} else if ('blob' in content && typeof content.blob === 'string') {
console.log(` [Binary data: ${content.blob.length} bytes]`);
}
}
} catch (error) {
console.log(`Error reading resource ${uri}: ${error}`);
}
}

async function cleanup(): Promise<void> {
if (client && transport) {
try {
Expand Down
95 changes: 94 additions & 1 deletion src/examples/server/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult } from '../../types.js';
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult, ResourceLink } from '../../types.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
import { OAuthMetadata } from 'src/shared/auth.js';
Expand Down Expand Up @@ -173,6 +173,99 @@ const getServer = () => {
};
}
);

// Create additional resources for ResourceLink demonstration
server.registerResource(
'example-file-1',
'file:///example/file1.txt',
{
title: 'Example File 1',
description: 'First example file for ResourceLink demonstration',
mimeType: 'text/plain'
},
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'file:///example/file1.txt',
text: 'This is the content of file 1',
},
],
};
}
);

server.registerResource(
'example-file-2',
'file:///example/file2.txt',
{
title: 'Example File 2',
description: 'Second example file for ResourceLink demonstration',
mimeType: 'text/plain'
},
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'file:///example/file2.txt',
text: 'This is the content of file 2',
},
],
};
}
);

// Register a tool that returns ResourceLinks
server.registerTool(
'list-files',
{
title: 'List Files with ResourceLinks',
description: 'Returns a list of files as ResourceLinks without embedding their content',
inputSchema: {
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'),
},
},
async ({ includeDescriptions = true }): Promise<CallToolResult> => {
const resourceLinks: ResourceLink[] = [
{
type: 'resource_link',
uri: 'https://example.com/greetings/default',
name: 'Default Greeting',
mimeType: 'text/plain',
...(includeDescriptions && { description: 'A simple greeting resource' })
},
{
type: 'resource_link',
uri: 'file:///example/file1.txt',
name: 'Example File 1',
mimeType: 'text/plain',
...(includeDescriptions && { description: 'First example file for ResourceLink demonstration' })
},
{
type: 'resource_link',
uri: 'file:///example/file2.txt',
name: 'Example File 2',
mimeType: 'text/plain',
...(includeDescriptions && { description: 'Second example file for ResourceLink demonstration' })
}
];

return {
content: [
{
type: 'text',
text: 'Here are the available files as resource links:',
},
...resourceLinks,
{
type: 'text',
text: '\nYou can read any of these resources using their URI.',
}
],
};
}
);

return server;
};

Expand Down
Loading