Skip to content

Register resource #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ server.resource(
}]
})
);

// Structured resource registration with config object
server.registerResource(
"documentation",
{
uri: "docs://api/reference",
description: "API Reference Documentation",
metadata: {
mimeType: "text/markdown",
tags: ["reference", "api"]
}
},
async (uri) => ({
contents: [{
uri: uri.href,
text: "# API Reference\n\nThis is the API reference documentation.",
mimeType: "text/markdown"
}]
})
);
```

### Tools
Expand Down Expand Up @@ -167,6 +187,32 @@ server.tool(
};
}
);

// Structured tool registration with config object
server.registerTool(
"structured-tool",
{
description: "A tool registered with the structured approach",
inputSchema: {
query: z.string().min(3),
limit: z.number().optional()
},
outputSchema: {
results: z.array(z.string()),
count: z.number()
},
annotations: {
readOnlyHint: true,
title: "Search Tool"
}
},
async ({ query, limit = 10 }) => ({
structuredContent: {
results: [`Result for: ${query}`],
count: 1
}
})
);
```

### Prompts
Expand Down
147 changes: 147 additions & 0 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,67 @@ describe("resource()", () => {
expect(result.resources[0].name).toBe("test");
expect(result.resources[0].uri).toBe("test://resource");
});

/***
* Test: Resource Registration with registerResource
*/
test("should register resource with registerResource method", async () => {
const mcpServer = new McpServer({
name: "test server",
version: "1.0",
});
const client = new Client({
name: "test client",
version: "1.0",
});

mcpServer.registerResource("test-resource", {
uri: "test://registered-resource",
description: "A test resource registered with registerResource"
}, async () => ({
contents: [
{
uri: "test://registered-resource",
text: "Content from registered resource",
},
],
}));

const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();

await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);

// Check if resource is listed correctly
const listResult = await client.request(
{
method: "resources/list",
},
ListResourcesResultSchema,
);

expect(listResult.resources).toHaveLength(1);
expect(listResult.resources[0].name).toBe("test-resource");
expect(listResult.resources[0].uri).toBe("test://registered-resource");
expect(listResult.resources[0].description).toBe("A test resource registered with registerResource");

// Check if resource can be read
const readResult = await client.request(
{
method: "resources/read",
params: {
uri: "test://registered-resource",
},
},
ReadResourceResultSchema,
);

expect(readResult.contents).toHaveLength(1);
expect(readResult.contents[0].text).toBe("Content from registered resource");
});

/***
* Test: Update Resource with URI
Expand Down Expand Up @@ -2160,6 +2221,92 @@ describe("resource()", () => {
}));
}).toThrow(/already registered/);
});

/***
* Test: Preventing Duplicate Resource Registration with registerResource
*/
test("should prevent duplicate resource registration with registerResource", () => {
const mcpServer = new McpServer({
name: "test server",
version: "1.0",
});

mcpServer.resource("test", "test://resource", async () => ({
contents: [
{
uri: "test://resource",
text: "Test content",
},
],
}));

expect(() => {
mcpServer.registerResource("test2", {
uri: "test://resource",
description: "Duplicate resource"
}, async () => ({
contents: [
{
uri: "test://resource",
text: "Test content 2",
},
],
}));
}).toThrow(/already registered/);
});

/***
* Test: Resource Registration with registerResource and Metadata
*/
test("should register resource with registerResource and metadata", async () => {
const mcpServer = new McpServer({
name: "test server",
version: "1.0",
});
const client = new Client({
name: "test client",
version: "1.0",
});

mcpServer.registerResource("metadata-resource", {
uri: "test://metadata-resource",
description: "Resource with metadata",
metadata: {
mimeType: "application/json",
custom: "custom-value"
}
}, async () => ({
contents: [
{
uri: "test://metadata-resource",
text: "{\"test\": \"data\"}",
mimeType: "application/json"
},
],
}));

const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();

await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);

const result = await client.request(
{
method: "resources/list",
},
ListResourcesResultSchema,
);

expect(result.resources).toHaveLength(1);
expect(result.resources[0].name).toBe("metadata-resource");
expect(result.resources[0].uri).toBe("test://metadata-resource");
expect(result.resources[0].description).toBe("Resource with metadata");
expect(result.resources[0].mimeType).toBe("application/json");
expect(result.resources[0].custom).toBe("custom-value");
});

/***
* Test: Multiple Resource Registration
Expand Down
53 changes: 53 additions & 0 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,59 @@ export class McpServer {
)
}

/**
* Registers a resource with a config object and callback.
* Similar to the resource() method but with a more structured configuration.
*/
registerResource(
name: string,
config: {
uri: string;
description?: string;
metadata?: ResourceMetadata;
},
readCallback: ReadResourceCallback
): RegisteredResource {
if (this._registeredResources[config.uri]) {
throw new Error(`Resource ${config.uri} is already registered`);
}

const { uri, description, metadata } = config;
const resourceMetadata = { ...metadata };

// Add description to metadata if provided
if (description) {
resourceMetadata.description = description;
}

const registeredResource: RegisteredResource = {
name,
metadata: resourceMetadata,
readCallback,
enabled: true,
disable: () => registeredResource.update({ enabled: false }),
enable: () => registeredResource.update({ enabled: true }),
remove: () => registeredResource.update({ uri: null }),
update: (updates) => {
if (typeof updates.uri !== "undefined" && updates.uri !== uri) {
delete this._registeredResources[uri]
if (updates.uri) this._registeredResources[updates.uri] = registeredResource
}
if (typeof updates.name !== "undefined") registeredResource.name = updates.name
if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata
if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback
if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled
this.sendResourceListChanged()
},
};

this._registeredResources[uri] = registeredResource;
this.setResourceRequestHandlers();
this.sendResourceListChanged();

return registeredResource;
}

/**
* Registers a zero-argument prompt `name`, which will run the given function when the client calls it.
*/
Expand Down