Merge pull request #1197 from modelcontextprotocol/ashwin/resources

Add embedded resource reference example to everything server
This commit is contained in:
Ashwin Bhat
2025-04-07 08:27:01 -07:00
committed by GitHub
3 changed files with 167 additions and 35 deletions

20
src/everything/CLAUDE.md Normal file
View File

@@ -0,0 +1,20 @@
# MCP "Everything" Server - Development Guidelines
## Build, Test & Run Commands
- Build: `npm run build` - Compiles TypeScript to JavaScript
- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically
- Run server: `npm run start` - Starts the MCP server using stdio transport
- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport
- Prepare release: `npm run prepare` - Builds the project for publishing
## Code Style Guidelines
- Use ES modules with `.js` extension in import paths
- Strictly type all functions and variables with TypeScript
- Follow zod schema patterns for tool input validation
- Prefer async/await over callbacks and Promise chains
- Place all imports at top of file, grouped by external then internal
- Use descriptive variable names that clearly indicate purpose
- Implement proper cleanup for timers and resources in server shutdown
- Follow camelCase for variables/functions, PascalCase for types/classes, UPPER_CASE for constants
- Handle errors with try/catch blocks and provide clear error messages
- Use consistent indentation (2 spaces) and trailing commas in multi-line objects

View File

@@ -63,6 +63,15 @@ This MCP server attempts to exercise all the features of the MCP protocol. It is
} }
``` ```
8. `getResourceReference`
- Returns a resource reference that can be used by MCP clients
- Inputs:
- `resourceId` (number, 1-100): ID of the resource to reference
- Returns: A resource reference with:
- Text introduction
- Embedded resource with `type: "resource"`
- Text instruction for using the resource URI
### Resources ### Resources
The server provides 100 test resources in two formats: The server provides 100 test resources in two formats:
@@ -96,6 +105,13 @@ Resource features:
- `style` (string): Output style preference - `style` (string): Output style preference
- Returns: Multi-turn conversation with images - Returns: Multi-turn conversation with images
3. `resource_prompt`
- Demonstrates embedding resource references in prompts
- Required arguments:
- `resourceId` (number): ID of the resource to embed (1-100)
- Returns: Multi-turn conversation with an embedded resource reference
- Shows how to include resources directly in prompt messages
### Logging ### Logging
The server sends random-leveled log messages every 15 seconds, e.g.: The server sends random-leveled log messages every 15 seconds, e.g.:

View File

@@ -62,10 +62,21 @@ const EXAMPLE_COMPLETIONS = {
const GetTinyImageSchema = z.object({}); const GetTinyImageSchema = z.object({});
const AnnotatedMessageSchema = z.object({ const AnnotatedMessageSchema = z.object({
messageType: z.enum(["error", "success", "debug"]) messageType: z
.enum(["error", "success", "debug"])
.describe("Type of message to demonstrate different annotation patterns"), .describe("Type of message to demonstrate different annotation patterns"),
includeImage: z.boolean().default(false) includeImage: z
.describe("Whether to include an example image") .boolean()
.default(false)
.describe("Whether to include an example image"),
});
const GetResourceReferenceSchema = z.object({
resourceId: z
.number()
.min(1)
.max(100)
.describe("ID of the resource to reference (1-100)"),
}); });
enum ToolName { enum ToolName {
@@ -76,11 +87,13 @@ enum ToolName {
SAMPLE_LLM = "sampleLLM", SAMPLE_LLM = "sampleLLM",
GET_TINY_IMAGE = "getTinyImage", GET_TINY_IMAGE = "getTinyImage",
ANNOTATED_MESSAGE = "annotatedMessage", ANNOTATED_MESSAGE = "annotatedMessage",
GET_RESOURCE_REFERENCE = "getResourceReference",
} }
enum PromptName { enum PromptName {
SIMPLE = "simple_prompt", SIMPLE = "simple_prompt",
COMPLEX = "complex_prompt", COMPLEX = "complex_prompt",
RESOURCE = "resource_prompt",
} }
export const createServer = () => { export const createServer = () => {
@@ -96,7 +109,7 @@ export const createServer = () => {
tools: {}, tools: {},
logging: {}, logging: {},
}, },
}, }
); );
let subscriptions: Set<string> = new Set(); let subscriptions: Set<string> = new Set();
@@ -122,29 +135,30 @@ export const createServer = () => {
{ level: "error", data: "Error-level message" }, { level: "error", data: "Error-level message" },
{ level: "critical", data: "Critical-level message" }, { level: "critical", data: "Critical-level message" },
{ level: "alert", data: "Alert level-message" }, { level: "alert", data: "Alert level-message" },
{level: "emergency", data: "Emergency-level message"} { level: "emergency", data: "Emergency-level message" },
] ];
const isMessageIgnored = (level: LoggingLevel): boolean => { const isMessageIgnored = (level: LoggingLevel): boolean => {
const currentLevel = messages.findIndex((msg) => logLevel === msg.level); const currentLevel = messages.findIndex((msg) => logLevel === msg.level);
const messageLevel = messages.findIndex((msg) => level === msg.level); const messageLevel = messages.findIndex((msg) => level === msg.level);
return messageLevel < currentLevel; return messageLevel < currentLevel;
} };
// Set up update interval for random log messages // Set up update interval for random log messages
logsUpdateInterval = setInterval(() => { logsUpdateInterval = setInterval(() => {
let message = { let message = {
method: "notifications/message", method: "notifications/message",
params: messages[Math.floor(Math.random() * messages.length)], params: messages[Math.floor(Math.random() * messages.length)],
} };
if (!isMessageIgnored(message.params.level as LoggingLevel)) server.notification(message); if (!isMessageIgnored(message.params.level as LoggingLevel))
server.notification(message);
}, 15000); }, 15000);
// Helper method to request sampling from client // Helper method to request sampling from client
const requestSampling = async ( const requestSampling = async (
context: string, context: string,
uri: string, uri: string,
maxTokens: number = 100, maxTokens: number = 100
) => { ) => {
const request: CreateMessageRequest = { const request: CreateMessageRequest = {
method: "sampling/createMessage", method: "sampling/createMessage",
@@ -280,6 +294,17 @@ export const createServer = () => {
}, },
], ],
}, },
{
name: PromptName.RESOURCE,
description: "A prompt that includes an embedded resource reference",
arguments: [
{
name: "resourceId",
description: "Resource ID to include (1-100)",
required: true,
},
],
},
], ],
}; };
}); });
@@ -330,6 +355,37 @@ export const createServer = () => {
}; };
} }
if (name === PromptName.RESOURCE) {
const resourceId = parseInt(args?.resourceId as string, 10);
if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) {
throw new Error(
`Invalid resourceId: ${args?.resourceId}. Must be a number between 1 and 100.`
);
}
const resourceIndex = resourceId - 1;
const resource = ALL_RESOURCES[resourceIndex];
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`,
},
},
{
role: "user",
content: {
type: "resource",
resource: resource,
},
},
],
};
}
throw new Error(`Unknown prompt: ${name}`); throw new Error(`Unknown prompt: ${name}`);
}); });
@@ -347,7 +403,8 @@ export const createServer = () => {
}, },
{ {
name: ToolName.PRINT_ENV, name: ToolName.PRINT_ENV,
description: "Prints all environment variables, helpful for debugging MCP server configuration", description:
"Prints all environment variables, helpful for debugging MCP server configuration",
inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput,
}, },
{ {
@@ -368,9 +425,16 @@ export const createServer = () => {
}, },
{ {
name: ToolName.ANNOTATED_MESSAGE, name: ToolName.ANNOTATED_MESSAGE,
description: "Demonstrates how annotations can be used to provide metadata about content", description:
"Demonstrates how annotations can be used to provide metadata about content",
inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput,
}, },
{
name: ToolName.GET_RESOURCE_REFERENCE,
description:
"Returns a resource reference that can be used by MCP clients",
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
},
]; ];
return { tools }; return { tools };
@@ -407,7 +471,7 @@ export const createServer = () => {
for (let i = 1; i < steps + 1; i++) { for (let i = 1; i < steps + 1; i++) {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, stepDuration * 1000), setTimeout(resolve, stepDuration * 1000)
); );
if (progressToken !== undefined) { if (progressToken !== undefined) {
@@ -450,10 +514,12 @@ export const createServer = () => {
const result = await requestSampling( const result = await requestSampling(
prompt, prompt,
ToolName.SAMPLE_LLM, ToolName.SAMPLE_LLM,
maxTokens, maxTokens
); );
return { return {
content: [{ type: "text", text: `LLM sampling result: ${result.content.text}` }], content: [
{ type: "text", text: `LLM sampling result: ${result.content.text}` },
],
}; };
} }
@@ -478,6 +544,35 @@ export const createServer = () => {
}; };
} }
if (name === ToolName.GET_RESOURCE_REFERENCE) {
const validatedArgs = GetResourceReferenceSchema.parse(args);
const resourceId = validatedArgs.resourceId;
const resourceIndex = resourceId - 1;
if (resourceIndex < 0 || resourceIndex >= ALL_RESOURCES.length) {
throw new Error(`Resource with ID ${resourceId} does not exist`);
}
const resource = ALL_RESOURCES[resourceIndex];
return {
content: [
{
type: "text",
text: `Returning resource reference for Resource ${resourceId}:`,
},
{
type: "resource",
resource: resource,
},
{
type: "text",
text: `You can access this resource using the URI: ${resource.uri}`,
},
],
};
}
if (name === ToolName.ANNOTATED_MESSAGE) { if (name === ToolName.ANNOTATED_MESSAGE) {
const { messageType, includeImage } = AnnotatedMessageSchema.parse(args); const { messageType, includeImage } = AnnotatedMessageSchema.parse(args);
@@ -490,8 +585,8 @@ export const createServer = () => {
text: "Error: Operation failed", text: "Error: Operation failed",
annotations: { annotations: {
priority: 1.0, // Errors are highest priority priority: 1.0, // Errors are highest priority
audience: ["user", "assistant"] // Both need to know about errors audience: ["user", "assistant"], // Both need to know about errors
} },
}); });
} else if (messageType === "success") { } else if (messageType === "success") {
content.push({ content.push({
@@ -499,8 +594,8 @@ export const createServer = () => {
text: "Operation completed successfully", text: "Operation completed successfully",
annotations: { annotations: {
priority: 0.7, // Success messages are important but not critical priority: 0.7, // Success messages are important but not critical
audience: ["user"] // Success mainly for user consumption audience: ["user"], // Success mainly for user consumption
} },
}); });
} else if (messageType === "debug") { } else if (messageType === "debug") {
content.push({ content.push({
@@ -508,8 +603,8 @@ export const createServer = () => {
text: "Debug: Cache hit ratio 0.95, latency 150ms", text: "Debug: Cache hit ratio 0.95, latency 150ms",
annotations: { annotations: {
priority: 0.3, // Debug info is low priority priority: 0.3, // Debug info is low priority
audience: ["assistant"] // Technical details for assistant audience: ["assistant"], // Technical details for assistant
} },
}); });
} }
@@ -521,8 +616,8 @@ export const createServer = () => {
mimeType: "image/png", mimeType: "image/png",
annotations: { annotations: {
priority: 0.5, priority: 0.5,
audience: ["user"] // Images primarily for user visualization audience: ["user"], // Images primarily for user visualization
} },
}); });
} }
@@ -540,7 +635,7 @@ export const createServer = () => {
if (!resourceId) return { completion: { values: [] } }; if (!resourceId) return { completion: { values: [] } };
// Filter resource IDs that start with the input value // Filter resource IDs that start with the input value
const values = EXAMPLE_COMPLETIONS.resourceId.filter(id => const values = EXAMPLE_COMPLETIONS.resourceId.filter((id) =>
id.startsWith(argument.value) id.startsWith(argument.value)
); );
return { completion: { values, hasMore: false, total: values.length } }; return { completion: { values, hasMore: false, total: values.length } };
@@ -548,10 +643,11 @@ export const createServer = () => {
if (ref.type === "ref/prompt") { if (ref.type === "ref/prompt") {
// Handle completion for prompt arguments // Handle completion for prompt arguments
const completions = EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS]; const completions =
EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS];
if (!completions) return { completion: { values: [] } }; if (!completions) return { completion: { values: [] } };
const values = completions.filter(value => const values = completions.filter((value) =>
value.startsWith(argument.value) value.startsWith(argument.value)
); );
return { completion: { values, hasMore: false, total: values.length } }; return { completion: { values, hasMore: false, total: values.length } };