feat: replace array index-based resource URIs with UUID-based semantic paths

Implements @domdomegg's proposal to use UUIDs for resource identification instead of array indices:

- Generate UUIDs for all 100 resources (50 text, 50 blob)
- Use semantic paths: test://static/resource/text/{uuid} and test://static/resource/blob/{uuid}
- Remove dependency on array indices for URI construction
- Update resource templates to reflect new URI patterns
- Modify getResourceReference tool to accept resourceUri instead of resourceId
- Update resource_prompt argument from resourceId to resourceUri
- Improve resource link descriptions to mention UUID-based identification
- Update README documentation to reflect UUID-based system

This eliminates the confusion between array indices (0-based) and URI numbers (1-based)
while providing more intuitive resource categorization through semantic paths.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: adam jones <domdomegg@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-08-22 00:46:39 +00:00
parent 1add4cab77
commit 9d1a63cd42
2 changed files with 86 additions and 73 deletions

View File

@@ -66,7 +66,7 @@ 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
- `resourceUri` (string): URI of the resource to reference (e.g., test://static/resource/text/{uuid})
- Returns: A resource reference with:
- Text introduction
- Embedded resource with `type: "resource"`
@@ -91,18 +91,23 @@ This MCP server attempts to exercise all the features of the MCP protocol. It is
### Resources
The server provides 100 test resources in two formats:
- Even numbered resources:
- Plaintext format
- URI pattern: `test://static/resource/{even_number}`
- Content: Simple text description
The server provides 100 test resources (50 text, 50 blob) using UUID-based identifiers:
- Odd numbered resources:
- Text resources:
- Plaintext format
- URI pattern: `test://static/resource/text/{uuid}`
- Content: Simple text description with UUID
- Names: "Text Resource 1" through "Text Resource 50"
- Blob resources:
- Binary blob format
- URI pattern: `test://static/resource/{odd_number}`
- Content: Base64 encoded binary data
- URI pattern: `test://static/resource/blob/{uuid}`
- Content: Base64 encoded binary data with UUID
- Names: "Blob Resource 1" through "Blob Resource 50"
Resource features:
- UUID-based identifiers eliminate index/URI confusion
- Semantic paths clearly indicate resource type (text vs blob)
- Supports pagination (10 items per page)
- Allows subscribing to resource updates
- Demonstrates resource templates
@@ -125,7 +130,7 @@ Resource features:
3. `resource_prompt`
- Demonstrates embedding resource references in prompts
- Required arguments:
- `resourceId` (number): ID of the resource to embed (1-100)
- `resourceUri` (string): URI of the resource to embed (e.g., test://static/resource/text/{uuid})
- Returns: Multi-turn conversation with an embedded resource reference
- Shows how to include resources directly in prompt messages

View File

@@ -23,6 +23,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { randomUUID } from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -78,11 +79,9 @@ const AnnotatedMessageSchema = z.object({
});
const GetResourceReferenceSchema = z.object({
resourceId: z
.number()
.min(1)
.max(100)
.describe("ID of the resource to reference (1-100)"),
resourceUri: z
.string()
.describe("URI of the resource to reference (e.g., test://static/resource/text/{uuid})"),
});
const ElicitationSchema = z.object({});
@@ -91,9 +90,9 @@ const GetResourceLinksSchema = z.object({
count: z
.number()
.min(1)
.max(10)
.default(3)
.describe("Number of resource links to return (1-10)"),
.max(100)
.default(5)
.describe("Number of resource links to return (1-100)"),
});
const StructuredContentSchema = {
@@ -138,11 +137,11 @@ enum PromptName {
RESOURCE = "resource_prompt",
}
// Example completion values
const EXAMPLE_COMPLETIONS = {
style: ["casual", "formal", "technical", "friendly"],
temperature: ["0", "0.5", "0.7", "1.0"],
resourceId: ["1", "2", "3", "4", "5"],
// Example completion values - initialized after ALL_RESOURCES is defined
let EXAMPLE_COMPLETIONS: {
style: string[];
temperature: string[];
resourceUri: string[];
};
export const createServer = () => {
@@ -258,28 +257,36 @@ export const createServer = () => {
return await server.request(request, z.any());
};
const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => {
const uri = `test://static/resource/${i}`;
if (i % 2 === 0) {
return {
uri,
name: `Resource ${i + 1}`,
mimeType: "text/plain",
text: `Resource ${i + 1}: This is a plaintext resource`,
};
} else {
const buffer = Buffer.from(`Resource ${i + 1}: This is a base64 blob`);
return {
uri,
name: `Resource ${i + 1}`,
mimeType: "application/octet-stream",
blob: buffer.toString("base64"),
};
}
});
// Generate UUIDs for resources (50 text, 50 blob)
const TEXT_RESOURCE_UUIDS = Array.from({ length: 50 }, () => randomUUID());
const BLOB_RESOURCE_UUIDS = Array.from({ length: 50 }, () => randomUUID());
const ALL_RESOURCES: Resource[] = [
// Text resources
...TEXT_RESOURCE_UUIDS.map((uuid, i) => ({
uri: `test://static/resource/text/${uuid}`,
name: `Text Resource ${i + 1}`,
mimeType: "text/plain",
text: `Text Resource ${i + 1}: This is a plaintext resource with UUID ${uuid}`,
} as Resource)),
// Blob resources
...BLOB_RESOURCE_UUIDS.map((uuid, i) => ({
uri: `test://static/resource/blob/${uuid}`,
name: `Blob Resource ${i + 1}`,
mimeType: "application/octet-stream",
blob: Buffer.from(`Blob Resource ${i + 1}: This is a base64 blob with UUID ${uuid}`).toString("base64"),
} as Resource)),
];
const PAGE_SIZE = 10;
// Initialize completion values after ALL_RESOURCES is defined
EXAMPLE_COMPLETIONS = {
style: ["casual", "formal", "technical", "friendly"],
temperature: ["0", "0.5", "0.7", "1.0"],
resourceUri: ALL_RESOURCES.slice(0, 5).map(r => r.uri),
};
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
const cursor = request.params?.cursor;
let startIndex = 0;
@@ -309,9 +316,14 @@ export const createServer = () => {
return {
resourceTemplates: [
{
uriTemplate: "test://static/resource/{id}",
name: "Static Resource",
description: "A static resource with a numeric ID",
uriTemplate: "test://static/resource/text/{uuid}",
name: "Text Resource",
description: "A static text resource with a UUID",
},
{
uriTemplate: "test://static/resource/blob/{uuid}",
name: "Blob Resource",
description: "A static blob resource with a UUID",
},
],
};
@@ -321,9 +333,8 @@ export const createServer = () => {
const uri = request.params.uri;
if (uri.startsWith("test://static/resource/")) {
const index = parseInt(uri.split("/").pop() ?? "", 10);
if (index >= 0 && index < ALL_RESOURCES.length) {
const resource = ALL_RESOURCES[index];
const resource = ALL_RESOURCES.find(r => r.uri === uri);
if (resource) {
return {
contents: [resource],
};
@@ -375,8 +386,8 @@ export const createServer = () => {
description: "A prompt that includes an embedded resource reference",
arguments: [
{
name: "resourceId",
description: "Resource ID to include (1-100)",
name: "resourceUri",
description: "Resource URI to include (e.g., test://static/resource/text/{uuid})",
required: true,
},
],
@@ -432,15 +443,17 @@ export const createServer = () => {
}
if (name === PromptName.RESOURCE) {
const resourceId = parseInt(args?.resourceId as string, 10);
if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) {
const resourceUri = args?.resourceUri as string;
if (!resourceUri) {
throw new Error(
`Invalid resourceId: ${args?.resourceId}. Must be a number between 1 and 100.`
`Invalid resourceUri: ${args?.resourceUri}. Must be a valid resource URI.`
);
}
const resourceIndex = resourceId - 1;
const resource = ALL_RESOURCES[resourceIndex];
const resource = ALL_RESOURCES.find(r => r.uri === resourceUri);
if (!resource) {
throw new Error(`Resource with URI ${resourceUri} does not exist`);
}
return {
messages: [
@@ -448,7 +461,7 @@ export const createServer = () => {
role: "user",
content: {
type: "text",
text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`,
text: `This prompt includes ${resource.name}. Please analyze the following resource:`,
},
},
{
@@ -691,20 +704,18 @@ export const createServer = () => {
if (name === ToolName.GET_RESOURCE_REFERENCE) {
const validatedArgs = GetResourceReferenceSchema.parse(args);
const resourceId = validatedArgs.resourceId;
const resourceUri = validatedArgs.resourceUri;
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.find(r => r.uri === resourceUri);
if (!resource) {
throw new Error(`Resource with URI ${resourceUri} does not exist`);
}
const resource = ALL_RESOURCES[resourceIndex];
return {
content: [
{
type: "text",
text: `Returning resource reference for Resource ${resourceId}:`,
text: `Returning resource reference for ${resource.name}:`,
},
{
type: "resource",
@@ -791,10 +802,10 @@ export const createServer = () => {
type: "resource_link",
uri: resource.uri,
name: resource.name,
description: `Resource ${i + 1}: ${
description: `${resource.name}: ${
resource.mimeType === "text/plain"
? "plaintext resource"
: "binary blob resource"
? "plaintext resource with UUID"
: "binary blob resource with UUID"
}`,
mimeType: resource.mimeType,
});
@@ -831,12 +842,9 @@ export const createServer = () => {
const { ref, argument } = request.params;
if (ref.type === "ref/resource") {
const resourceId = ref.uri.split("/").pop();
if (!resourceId) return { completion: { values: [] } };
// Filter resource IDs that start with the input value
const values = EXAMPLE_COMPLETIONS.resourceId.filter((id) =>
id.startsWith(argument.value)
// Filter resource URIs that contain the input value
const values = EXAMPLE_COMPLETIONS.resourceUri.filter((uri) =>
uri.toLowerCase().includes(argument.value.toLowerCase())
);
return { completion: { values, hasMore: false, total: values.length } };
}
@@ -848,7 +856,7 @@ export const createServer = () => {
if (!completions) return { completion: { values: [] } };
const values = completions.filter((value) =>
value.startsWith(argument.value)
value.toLowerCase().includes(argument.value.toLowerCase())
);
return { completion: { values, hasMore: false, total: values.length } };
}