Skip to content

Commit 8635c63

Browse files
committed
Autocomplete on prompt arguments
1 parent 02fa787 commit 8635c63

File tree

2 files changed

+192
-2
lines changed

2 files changed

+192
-2
lines changed

src/server/index.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import {
2222
ReadResourceResultSchema,
2323
ListPromptsResultSchema,
2424
GetPromptResultSchema,
25+
CompleteResultSchema,
2526
} from "../types.js";
2627
import { Transport } from "../shared/transport.js";
2728
import { InMemoryTransport } from "../inMemory.js";
2829
import { Client } from "../client/index.js";
2930
import { UriTemplate } from "../shared/uriTemplate.js";
3031
import { ResourceTemplate } from "./index.js";
32+
import { completable } from "./completable.js";
3133

3234
test("should accept latest protocol version", async () => {
3335
let sendPromiseResolve: (value: unknown) => void;
@@ -1619,4 +1621,135 @@ describe("Server.prompt", () => {
16191621
),
16201622
).rejects.toThrow(/Prompt nonexistent-prompt not found/);
16211623
});
1624+
test("should support completion of prompt arguments", async () => {
1625+
const server = new Server({
1626+
name: "test server",
1627+
version: "1.0",
1628+
});
1629+
1630+
const client = new Client(
1631+
{
1632+
name: "test client",
1633+
version: "1.0",
1634+
},
1635+
{
1636+
capabilities: {
1637+
prompts: {},
1638+
},
1639+
},
1640+
);
1641+
1642+
server.prompt(
1643+
"test-prompt",
1644+
{
1645+
name: completable(z.string(), () => ["Alice", "Bob", "Charlie"]),
1646+
},
1647+
async ({ name }) => ({
1648+
messages: [
1649+
{
1650+
role: "assistant",
1651+
content: {
1652+
type: "text",
1653+
text: `Hello ${name}`,
1654+
},
1655+
},
1656+
],
1657+
}),
1658+
);
1659+
1660+
const [clientTransport, serverTransport] =
1661+
InMemoryTransport.createLinkedPair();
1662+
1663+
await Promise.all([
1664+
client.connect(clientTransport),
1665+
server.connect(serverTransport),
1666+
]);
1667+
1668+
const result = await client.request(
1669+
{
1670+
method: "completion/complete",
1671+
params: {
1672+
ref: {
1673+
type: "ref/prompt",
1674+
name: "test-prompt",
1675+
},
1676+
argument: {
1677+
name: "name",
1678+
value: "",
1679+
},
1680+
},
1681+
},
1682+
CompleteResultSchema,
1683+
);
1684+
1685+
expect(result.completion.values).toEqual(["Alice", "Bob", "Charlie"]);
1686+
expect(result.completion.total).toBe(3);
1687+
});
1688+
1689+
test("should support filtered completion of prompt arguments", async () => {
1690+
const server = new Server({
1691+
name: "test server",
1692+
version: "1.0",
1693+
});
1694+
1695+
const client = new Client(
1696+
{
1697+
name: "test client",
1698+
version: "1.0",
1699+
},
1700+
{
1701+
capabilities: {
1702+
prompts: {},
1703+
},
1704+
},
1705+
);
1706+
1707+
server.prompt(
1708+
"test-prompt",
1709+
{
1710+
name: completable(z.string(), (test) =>
1711+
["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)),
1712+
),
1713+
},
1714+
async ({ name }) => ({
1715+
messages: [
1716+
{
1717+
role: "assistant",
1718+
content: {
1719+
type: "text",
1720+
text: `Hello ${name}`,
1721+
},
1722+
},
1723+
],
1724+
}),
1725+
);
1726+
1727+
const [clientTransport, serverTransport] =
1728+
InMemoryTransport.createLinkedPair();
1729+
1730+
await Promise.all([
1731+
client.connect(clientTransport),
1732+
server.connect(serverTransport),
1733+
]);
1734+
1735+
const result = await client.request(
1736+
{
1737+
method: "completion/complete",
1738+
params: {
1739+
ref: {
1740+
type: "ref/prompt",
1741+
name: "test-prompt",
1742+
},
1743+
argument: {
1744+
name: "name",
1745+
value: "A",
1746+
},
1747+
},
1748+
},
1749+
CompleteResultSchema,
1750+
);
1751+
1752+
expect(result.completion.values).toEqual(["Alice"]);
1753+
expect(result.completion.total).toBe(1);
1754+
});
16221755
});

src/server/index.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import z, {
44
ZodOptional,
55
ZodRawShape,
66
ZodString,
7+
ZodType,
78
ZodTypeAny,
9+
ZodTypeDef,
810
} from "zod";
911
import { zodToJsonSchema } from "zod-to-json-schema";
1012
import {
@@ -18,6 +20,8 @@ import {
1820
CallToolRequestSchema,
1921
CallToolResult,
2022
ClientCapabilities,
23+
CompleteRequestSchema,
24+
CompleteResult,
2125
CreateMessageRequest,
2226
CreateMessageResultSchema,
2327
EmptyResultSchema,
@@ -58,6 +62,7 @@ import {
5862
Tool,
5963
} from "../types.js";
6064
import { UriTemplate, Variables } from "../shared/uriTemplate.js";
65+
import { Completable, CompletableDef } from "./completable.js";
6166

6267
export type ServerOptions = ProtocolOptions & {
6368
/**
@@ -703,6 +708,7 @@ export class Server<
703708
ListPromptsRequestSchema.shape.method.value,
704709
);
705710
this.assertCanSetRequestHandler(GetPromptRequestSchema.shape.method.value);
711+
this.assertCanSetRequestHandler(CompleteRequestSchema.shape.method.value);
706712

707713
this.registerCapabilities({
708714
prompts: {},
@@ -756,6 +762,56 @@ export class Server<
756762
}
757763
},
758764
);
765+
766+
this.setRequestHandler(
767+
CompleteRequestSchema,
768+
async (request): Promise<CompleteResult> => {
769+
if (request.params.ref.type !== "ref/prompt") {
770+
throw new McpError(
771+
ErrorCode.InvalidParams,
772+
"Only prompt completions are supported",
773+
);
774+
}
775+
776+
const prompt = this._registeredPrompts[request.params.ref.name];
777+
if (!prompt) {
778+
throw new McpError(
779+
ErrorCode.InvalidParams,
780+
`Prompt ${request.params.ref.name} not found`,
781+
);
782+
}
783+
784+
if (!prompt.argsSchema) {
785+
return {
786+
completion: {
787+
values: [],
788+
hasMore: false,
789+
},
790+
};
791+
}
792+
793+
const field = prompt.argsSchema.shape[request.params.argument.name];
794+
if (!(field instanceof Completable)) {
795+
return {
796+
completion: {
797+
values: [],
798+
hasMore: false,
799+
},
800+
};
801+
}
802+
803+
const def: CompletableDef<ZodString> = field._def;
804+
const completer = def.complete;
805+
const suggestions = await completer(request.params.argument.value);
806+
return {
807+
completion: {
808+
values: suggestions.slice(0, 100),
809+
total: suggestions.length,
810+
hasMore: suggestions.length > 100,
811+
},
812+
};
813+
},
814+
);
759815
}
760816

761817
prompt(name: string, cb: PromptCallback): void;
@@ -765,7 +821,6 @@ export class Server<
765821
argsSchema: Args,
766822
cb: PromptCallback<Args>,
767823
): void;
768-
769824
prompt<Args extends PromptArgsRawShape>(
770825
name: string,
771826
description: string,
@@ -896,7 +951,9 @@ type RegisteredResourceTemplate = {
896951
};
897952

898953
type PromptArgsRawShape = {
899-
[k: string]: ZodString | ZodOptional<ZodString>;
954+
[k: string]:
955+
| ZodType<string, ZodTypeDef, string>
956+
| ZodOptional<ZodType<string, ZodTypeDef, string>>;
900957
};
901958

902959
export type PromptCallback<

0 commit comments

Comments
 (0)