Skip to content

Commit c90d6c9

Browse files
committed
add context with arguments to completable
1 parent f822c12 commit c90d6c9

File tree

5 files changed

+357
-22
lines changed

5 files changed

+357
-22
lines changed

src/server/completable.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export enum McpZodTypeKind {
1515

1616
export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
1717
value: T["_input"],
18+
context?: {
19+
arguments?: Record<string, string>;
20+
},
1821
) => T["_input"][] | Promise<T["_input"][]>;
1922

2023
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny>

src/server/mcp.test.ts

Lines changed: 252 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3521,12 +3521,12 @@ describe("prompt()", () => {
35213521
);
35223522

35233523
expect(result.resources).toHaveLength(2);
3524-
3524+
35253525
// Resource 1 should have its own metadata
35263526
expect(result.resources[0].name).toBe("Resource 1");
35273527
expect(result.resources[0].description).toBe("Individual resource description");
35283528
expect(result.resources[0].mimeType).toBe("text/plain");
3529-
3529+
35303530
// Resource 2 should inherit template metadata
35313531
expect(result.resources[1].name).toBe("Resource 2");
35323532
expect(result.resources[1].description).toBe("Template description");
@@ -3592,7 +3592,7 @@ describe("prompt()", () => {
35923592
);
35933593

35943594
expect(result.resources).toHaveLength(1);
3595-
3595+
35963596
// All fields should be from the individual resource, not the template
35973597
expect(result.resources[0].name).toBe("Overridden Name");
35983598
expect(result.resources[0].description).toBe("Overridden description");
@@ -3698,41 +3698,274 @@ describe("Tool title precedence", () => {
36983698
});
36993699

37003700
test("getDisplayName unit tests for title precedence", () => {
3701-
3701+
37023702
// Test 1: Only name
37033703
expect(getDisplayName({ name: "tool_name" })).toBe("tool_name");
3704-
3704+
37053705
// Test 2: Name and title - title wins
3706-
expect(getDisplayName({
3707-
name: "tool_name",
3708-
title: "Tool Title"
3706+
expect(getDisplayName({
3707+
name: "tool_name",
3708+
title: "Tool Title"
37093709
})).toBe("Tool Title");
3710-
3710+
37113711
// Test 3: Name and annotations.title - annotations.title wins
3712-
expect(getDisplayName({
3712+
expect(getDisplayName({
37133713
name: "tool_name",
37143714
annotations: { title: "Annotations Title" }
37153715
})).toBe("Annotations Title");
3716-
3716+
37173717
// Test 4: All three - title wins (correct precedence)
3718-
expect(getDisplayName({
3719-
name: "tool_name",
3718+
expect(getDisplayName({
3719+
name: "tool_name",
37203720
title: "Regular Title",
37213721
annotations: { title: "Annotations Title" }
37223722
})).toBe("Regular Title");
3723-
3723+
37243724
// Test 5: Empty title should not be used
3725-
expect(getDisplayName({
3726-
name: "tool_name",
3725+
expect(getDisplayName({
3726+
name: "tool_name",
37273727
title: "",
37283728
annotations: { title: "Annotations Title" }
37293729
})).toBe("Annotations Title");
3730-
3730+
37313731
// Test 6: Undefined vs null handling
3732-
expect(getDisplayName({
3733-
name: "tool_name",
3732+
expect(getDisplayName({
3733+
name: "tool_name",
37343734
title: undefined,
37353735
annotations: { title: "Annotations Title" }
37363736
})).toBe("Annotations Title");
37373737
});
3738+
3739+
test("should support resource template completion with resolved context", async () => {
3740+
const mcpServer = new McpServer({
3741+
name: "test server",
3742+
version: "1.0",
3743+
});
3744+
3745+
const client = new Client({
3746+
name: "test client",
3747+
version: "1.0",
3748+
});
3749+
3750+
mcpServer.resource(
3751+
"test",
3752+
new ResourceTemplate("github://repos/{owner}/{repo}", {
3753+
list: undefined,
3754+
complete: {
3755+
repo: (value, context) => {
3756+
if (context?.arguments?.["owner"] === "org1") {
3757+
return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
3758+
} else if (context?.arguments?.["owner"] === "org2") {
3759+
return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value));
3760+
}
3761+
return [];
3762+
},
3763+
},
3764+
}),
3765+
async () => ({
3766+
contents: [
3767+
{
3768+
uri: "github://repos/test/test",
3769+
text: "Test content",
3770+
},
3771+
],
3772+
}),
3773+
);
3774+
3775+
const [clientTransport, serverTransport] =
3776+
InMemoryTransport.createLinkedPair();
3777+
3778+
await Promise.all([
3779+
client.connect(clientTransport),
3780+
mcpServer.server.connect(serverTransport),
3781+
]);
3782+
3783+
// Test with microsoft owner
3784+
const result1 = await client.request(
3785+
{
3786+
method: "completion/complete",
3787+
params: {
3788+
ref: {
3789+
type: "ref/resource",
3790+
uri: "github://repos/{owner}/{repo}",
3791+
},
3792+
argument: {
3793+
name: "repo",
3794+
value: "p",
3795+
},
3796+
context: {
3797+
arguments: {
3798+
owner: "org1",
3799+
},
3800+
},
3801+
},
3802+
},
3803+
CompleteResultSchema,
3804+
);
3805+
3806+
expect(result1.completion.values).toEqual(["project1", "project2", "project3"]);
3807+
expect(result1.completion.total).toBe(3);
3808+
3809+
// Test with facebook owner
3810+
const result2 = await client.request(
3811+
{
3812+
method: "completion/complete",
3813+
params: {
3814+
ref: {
3815+
type: "ref/resource",
3816+
uri: "github://repos/{owner}/{repo}",
3817+
},
3818+
argument: {
3819+
name: "repo",
3820+
value: "r",
3821+
},
3822+
context: {
3823+
arguments: {
3824+
owner: "org2",
3825+
},
3826+
},
3827+
},
3828+
},
3829+
CompleteResultSchema,
3830+
);
3831+
3832+
expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]);
3833+
expect(result2.completion.total).toBe(3);
3834+
3835+
// Test with no resolved context
3836+
const result3 = await client.request(
3837+
{
3838+
method: "completion/complete",
3839+
params: {
3840+
ref: {
3841+
type: "ref/resource",
3842+
uri: "github://repos/{owner}/{repo}",
3843+
},
3844+
argument: {
3845+
name: "repo",
3846+
value: "t",
3847+
},
3848+
},
3849+
},
3850+
CompleteResultSchema,
3851+
);
3852+
3853+
expect(result3.completion.values).toEqual([]);
3854+
expect(result3.completion.total).toBe(0);
3855+
});
3856+
3857+
test("should support prompt argument completion with resolved context", async () => {
3858+
const mcpServer = new McpServer({
3859+
name: "test server",
3860+
version: "1.0",
3861+
});
3862+
3863+
const client = new Client({
3864+
name: "test client",
3865+
version: "1.0",
3866+
});
3867+
3868+
mcpServer.prompt(
3869+
"test-prompt",
3870+
{
3871+
name: completable(z.string(), (value, context) => {
3872+
if (context?.arguments?.["category"] === "developers") {
3873+
return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value));
3874+
} else if (context?.arguments?.["category"] === "managers") {
3875+
return ["David", "Eve", "Frank"].filter(n => n.startsWith(value));
3876+
}
3877+
return ["Guest"].filter(n => n.startsWith(value));
3878+
}),
3879+
},
3880+
async ({ name }) => ({
3881+
messages: [
3882+
{
3883+
role: "assistant",
3884+
content: {
3885+
type: "text",
3886+
text: `Hello ${name}`,
3887+
},
3888+
},
3889+
],
3890+
}),
3891+
);
3892+
3893+
const [clientTransport, serverTransport] =
3894+
InMemoryTransport.createLinkedPair();
3895+
3896+
await Promise.all([
3897+
client.connect(clientTransport),
3898+
mcpServer.server.connect(serverTransport),
3899+
]);
3900+
3901+
// Test with developers category
3902+
const result1 = await client.request(
3903+
{
3904+
method: "completion/complete",
3905+
params: {
3906+
ref: {
3907+
type: "ref/prompt",
3908+
name: "test-prompt",
3909+
},
3910+
argument: {
3911+
name: "name",
3912+
value: "A",
3913+
},
3914+
context: {
3915+
arguments: {
3916+
category: "developers",
3917+
},
3918+
},
3919+
},
3920+
},
3921+
CompleteResultSchema,
3922+
);
3923+
3924+
expect(result1.completion.values).toEqual(["Alice"]);
3925+
3926+
// Test with managers category
3927+
const result2 = await client.request(
3928+
{
3929+
method: "completion/complete",
3930+
params: {
3931+
ref: {
3932+
type: "ref/prompt",
3933+
name: "test-prompt",
3934+
},
3935+
argument: {
3936+
name: "name",
3937+
value: "D",
3938+
},
3939+
context: {
3940+
arguments: {
3941+
category: "managers",
3942+
},
3943+
},
3944+
},
3945+
},
3946+
CompleteResultSchema,
3947+
);
3948+
3949+
expect(result2.completion.values).toEqual(["David"]);
3950+
3951+
// Test with no resolved context
3952+
const result3 = await client.request(
3953+
{
3954+
method: "completion/complete",
3955+
params: {
3956+
ref: {
3957+
type: "ref/prompt",
3958+
name: "test-prompt",
3959+
},
3960+
argument: {
3961+
name: "name",
3962+
value: "G",
3963+
},
3964+
},
3965+
},
3966+
CompleteResultSchema,
3967+
);
3968+
3969+
expect(result3.completion.values).toEqual(["Guest"]);
3970+
});
37383971
});

src/server/mcp.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export class McpServer {
293293
}
294294

295295
const def: CompletableDef<ZodString> = field._def;
296-
const suggestions = await def.complete(request.params.argument.value);
296+
const suggestions = await def.complete(request.params.argument.value, request.params.context);
297297
return createCompletionResult(suggestions);
298298
}
299299

@@ -324,7 +324,7 @@ export class McpServer {
324324
return EMPTY_COMPLETION_RESULT;
325325
}
326326

327-
const suggestions = await completer(request.params.argument.value);
327+
const suggestions = await completer(request.params.argument.value, request.params.context);
328328
return createCompletionResult(suggestions);
329329
}
330330

@@ -1068,6 +1068,9 @@ export class McpServer {
10681068
*/
10691069
export type CompleteResourceTemplateCallback = (
10701070
value: string,
1071+
context?: {
1072+
arguments?: Record<string, string>;
1073+
},
10711074
) => string[] | Promise<string[]>;
10721075

10731076
/**

0 commit comments

Comments
 (0)