Skip to content

Commit 263f3f8

Browse files
committed
Add registerResources() bulk method
- Accepts array of resource configurations - Registers all resources with single notification - Supports both string URIs and ResourceTemplate objects - Consistent with existing registration patterns
1 parent 903bf38 commit 263f3f8

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

src/server/mcp.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4515,4 +4515,113 @@ describe("elicitInput()", () => {
45154515
text: "Bulk 1"
45164516
});
45174517
});
4518+
4519+
/***
4520+
* Test: registerResources() bulk method
4521+
*/
4522+
test("should register multiple resources with single notification using registerResources()", async () => {
4523+
const mcpServer = new McpServer({
4524+
name: "test server",
4525+
version: "1.0",
4526+
});
4527+
4528+
// Register first resource to establish capabilities
4529+
mcpServer.resource("initial", "initial://resource", async () => ({
4530+
contents: [{
4531+
uri: "initial://resource",
4532+
text: "Initial resource"
4533+
}]
4534+
}));
4535+
4536+
const notifications: Notification[] = []
4537+
const client = new Client({
4538+
name: "test client",
4539+
version: "1.0",
4540+
});
4541+
client.fallbackNotificationHandler = async (notification) => {
4542+
notifications.push(notification)
4543+
}
4544+
4545+
const [clientTransport, serverTransport] =
4546+
InMemoryTransport.createLinkedPair();
4547+
4548+
await Promise.all([
4549+
client.connect(clientTransport),
4550+
mcpServer.connect(serverTransport),
4551+
]);
4552+
4553+
// Clear initial notifications
4554+
notifications.length = 0;
4555+
4556+
// Register multiple resources at once using the bulk method
4557+
const resources = mcpServer.registerResources([
4558+
{
4559+
name: "bulk1",
4560+
uriOrTemplate: "bulk://resource1",
4561+
config: {
4562+
title: "Bulk Resource 1",
4563+
description: "First bulk resource"
4564+
},
4565+
callback: async () => ({
4566+
contents: [{
4567+
uri: "bulk://resource1",
4568+
text: "Bulk resource 1 content"
4569+
}]
4570+
})
4571+
},
4572+
{
4573+
name: "bulk2",
4574+
uriOrTemplate: "bulk://resource2",
4575+
config: {
4576+
title: "Bulk Resource 2",
4577+
description: "Second bulk resource"
4578+
},
4579+
callback: async () => ({
4580+
contents: [{
4581+
uri: "bulk://resource2",
4582+
text: "Bulk resource 2 content"
4583+
}]
4584+
})
4585+
}
4586+
]);
4587+
4588+
// Yield event loop to let notifications process
4589+
await new Promise(process.nextTick);
4590+
4591+
// Should have sent exactly ONE notification for all resources
4592+
expect(notifications).toHaveLength(1);
4593+
expect(notifications[0]).toMatchObject({
4594+
method: "notifications/resources/list_changed",
4595+
});
4596+
4597+
// Should return array of registered resources
4598+
expect(resources).toHaveLength(2);
4599+
expect(resources[0].title).toBe("Bulk Resource 1");
4600+
expect(resources[1].title).toBe("Bulk Resource 2");
4601+
4602+
// Verify all resources are registered and functional
4603+
const resourcesResult = await client.request(
4604+
{ method: "resources/list" },
4605+
ListResourcesResultSchema,
4606+
);
4607+
expect(resourcesResult.resources).toHaveLength(3); // initial + 2 bulk resources
4608+
4609+
const resourceNames = resourcesResult.resources.map(r => r.name).sort();
4610+
expect(resourceNames).toEqual(["bulk1", "bulk2", "initial"]);
4611+
4612+
// Test that a resource actually works
4613+
const readResult = await client.request(
4614+
{
4615+
method: "resources/read",
4616+
params: {
4617+
uri: "bulk://resource1"
4618+
}
4619+
},
4620+
ReadResourceResultSchema,
4621+
);
4622+
expect(readResult.contents[0]).toMatchObject({
4623+
uri: "bulk://resource1",
4624+
text: "Bulk resource 1 content"
4625+
});
4626+
});
45184627
});

src/server/mcp.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,62 @@ export class McpServer {
666666
}
667667
}
668668

669+
/**
670+
* Registers multiple resources at once with a single notification.
671+
* This is more efficient than calling registerResource() multiple times,
672+
* especially when registering many resources, as it sends only one list_changed notification.
673+
*/
674+
registerResources<T extends Array<{
675+
name: string;
676+
uriOrTemplate: string | ResourceTemplate;
677+
config: ResourceMetadata;
678+
callback: ReadResourceCallback | ReadResourceTemplateCallback;
679+
}>>(resources: T): (RegisteredResource | RegisteredResourceTemplate)[] {
680+
const results: (RegisteredResource | RegisteredResourceTemplate)[] = [];
681+
682+
// First, validate that none of the resources are already registered
683+
for (const { name, uriOrTemplate } of resources) {
684+
if (typeof uriOrTemplate === "string") {
685+
if (this._registeredResources[uriOrTemplate]) {
686+
throw new Error(`Resource ${uriOrTemplate} is already registered`);
687+
}
688+
} else {
689+
if (this._registeredResourceTemplates[name]) {
690+
throw new Error(`Resource template ${name} is already registered`);
691+
}
692+
}
693+
}
694+
695+
// Register all resources without sending notifications
696+
for (const { name, uriOrTemplate, config, callback } of resources) {
697+
if (typeof uriOrTemplate === "string") {
698+
const result = this._createRegisteredResource(
699+
name,
700+
(config as BaseMetadata).title,
701+
uriOrTemplate,
702+
config,
703+
callback as ReadResourceCallback
704+
);
705+
results.push(result);
706+
} else {
707+
const result = this._createRegisteredResourceTemplate(
708+
name,
709+
(config as BaseMetadata).title,
710+
uriOrTemplate,
711+
config,
712+
callback as ReadResourceTemplateCallback
713+
);
714+
results.push(result);
715+
}
716+
}
717+
718+
// Set up handlers and send single notification at the end
719+
this.setResourceRequestHandlers();
720+
this.sendResourceListChanged();
721+
722+
return results;
723+
}
724+
669725
private _createRegisteredResource(
670726
name: string,
671727
title: string | undefined,

0 commit comments

Comments
 (0)