Skip to content

Simplified, Express-like API #117

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

Merged
merged 33 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f8f5686
Server.tool() convenience API
jspahrsummers Jan 6, 2025
4bf5c53
Assert tool request handlers do not already exist
jspahrsummers Jan 7, 2025
1506bd1
Allow capabilities to be dynamically registered after initialization
jspahrsummers Jan 7, 2025
7ac5a5f
Automatically register `tools` capability
jspahrsummers Jan 7, 2025
c14a5d4
Tests for `mergeCapabilities`
jspahrsummers Jan 7, 2025
b538731
Fix string type check
jspahrsummers Jan 7, 2025
ff22f25
Tests for Server.tool
jspahrsummers Jan 7, 2025
aac1959
Errors invoking tools should be erroneous tool results, not McpErrors
jspahrsummers Jan 7, 2025
bb28c9b
Minor tweak to make test code more idiomatic
jspahrsummers Jan 7, 2025
7f0cf73
URI Template parser and matcher
jspahrsummers Jan 7, 2025
dc77d9c
More thorough handling of edge cases/pathologies
jspahrsummers Jan 7, 2025
1e040e9
Add method to determine if a URI is a template or not
jspahrsummers Jan 7, 2025
e4b3820
Add UriTemplate.toString
jspahrsummers Jan 7, 2025
098266a
Add simplified API for registering resources and resource templates
jspahrsummers Jan 7, 2025
1970509
Method documentation
jspahrsummers Jan 7, 2025
3f9acec
Add a test for listCallback
jspahrsummers Jan 7, 2025
45f99e6
Make `Variables` non-optional with resource template URIs
jspahrsummers Jan 7, 2025
8356cb9
Create `ResourceTemplate` class and move `listCallback` into it
jspahrsummers Jan 8, 2025
f090c93
Move `Server` helper definitions to bottom of file
jspahrsummers Jan 8, 2025
5ddfa0a
Add `extra` to resource request handlers
jspahrsummers Jan 8, 2025
9b95fa8
Server.prompt() convenience API
jspahrsummers Jan 8, 2025
02fa787
Add `completable` wrapper for Zod schemas
jspahrsummers Jan 8, 2025
8635c63
Autocomplete on prompt arguments
jspahrsummers Jan 8, 2025
189cc84
Autocomplete on resource template variables
jspahrsummers Jan 8, 2025
2289dbc
Factor out convenience APIs into another class
jspahrsummers Jan 8, 2025
7629c70
Remove unused import
jspahrsummers Jan 8, 2025
2e35b15
Merge branch 'main' into justin/simplified-api
jspahrsummers Jan 13, 2025
e60fb5e
Update README.md
jspahrsummers Jan 13, 2025
0790890
Model README on the Python SDK's README
jspahrsummers Jan 13, 2025
5a6823a
Stick with %20, don't replace with +
jspahrsummers Jan 16, 2025
ae5e297
Merge branch 'main' into justin/simplified-api
jspahrsummers Jan 20, 2025
d47a76f
Pre-emptively bump package version
jspahrsummers Jan 20, 2025
e8a5ffc
`npm audit fix`
jspahrsummers Jan 20, 2025
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
416 changes: 358 additions & 58 deletions README.md

Large diffs are not rendered by default.

23 changes: 17 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"dependencies": {
"content-type": "^1.0.5",
"raw-body": "^3.0.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
Expand Down
22 changes: 19 additions & 3 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
mergeCapabilities,
Protocol,
ProtocolOptions,
RequestOptions,
Expand Down Expand Up @@ -44,7 +45,7 @@ export type ClientOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this client.
*/
capabilities: ClientCapabilities;
capabilities?: ClientCapabilities;
};

/**
Expand Down Expand Up @@ -90,10 +91,25 @@ export class Client<
*/
constructor(
private _clientInfo: Implementation,
options: ClientOptions,
options?: ClientOptions,
) {
super(options);
this._capabilities = options.capabilities;
this._capabilities = options?.capabilities ?? {};
}

/**
* Registers new capabilities. This can only be called before connecting to a transport.
*
* The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
*/
public registerCapabilities(capabilities: ClientCapabilities): void {
if (this.transport) {
throw new Error(
"Cannot register capabilities after connecting to transport",
);
}

this._capabilities = mergeCapabilities(this._capabilities, capabilities);
}

protected assertCapability(
Expand Down
46 changes: 46 additions & 0 deletions src/server/completable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from "zod";
import { completable } from "./completable.js";

describe("completable", () => {
it("preserves types and values of underlying schema", () => {
const baseSchema = z.string();
const schema = completable(baseSchema, () => []);

expect(schema.parse("test")).toBe("test");
expect(() => schema.parse(123)).toThrow();
});

it("provides access to completion function", async () => {
const completions = ["foo", "bar", "baz"];
const schema = completable(z.string(), () => completions);

expect(await schema._def.complete("")).toEqual(completions);
});

it("allows async completion functions", async () => {
const completions = ["foo", "bar", "baz"];
const schema = completable(z.string(), async () => completions);

expect(await schema._def.complete("")).toEqual(completions);
});

it("passes current value to completion function", async () => {
const schema = completable(z.string(), (value) => [value + "!"]);

expect(await schema._def.complete("test")).toEqual(["test!"]);
});

it("works with number schemas", async () => {
const schema = completable(z.number(), () => [1, 2, 3]);

expect(schema.parse(1)).toBe(1);
expect(await schema._def.complete(0)).toEqual([1, 2, 3]);
});

it("preserves schema description", () => {
const desc = "test description";
const schema = completable(z.string().describe(desc), () => []);

expect(schema.description).toBe(desc);
});
});
95 changes: 95 additions & 0 deletions src/server/completable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
ZodTypeAny,
ZodTypeDef,
ZodType,
ParseInput,
ParseReturnType,
RawCreateParams,
ZodErrorMap,
ProcessedCreateParams,
} from "zod";

export enum McpZodTypeKind {
Completable = "McpCompletable",
}

export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
value: T["_input"],
) => T["_input"][] | Promise<T["_input"][]>;

export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
type: T;
complete: CompleteCallback<T>;
typeName: McpZodTypeKind.Completable;
}

export class Completable<T extends ZodTypeAny> extends ZodType<
T["_output"],
CompletableDef<T>,
T["_input"]
> {
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
return this._def.type._parse({
data,
path: ctx.path,
parent: ctx,
});
}

unwrap() {
return this._def.type;
}

static create = <T extends ZodTypeAny>(
type: T,
params: RawCreateParams & {
complete: CompleteCallback<T>;
},
): Completable<T> => {
return new Completable({
type,
typeName: McpZodTypeKind.Completable,
complete: params.complete,
...processCreateParams(params),
});
};
}

/**
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
*/
export function completable<T extends ZodTypeAny>(
schema: T,
complete: CompleteCallback<T>,
): Completable<T> {
return Completable.create(schema, { ...schema._def, complete });
}

// Not sure why this isn't exported from Zod:
// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130
function processCreateParams(params: RawCreateParams): ProcessedCreateParams {
if (!params) return {};
const { errorMap, invalid_type_error, required_error, description } = params;
if (errorMap && (invalid_type_error || required_error)) {
throw new Error(
`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`,
);
}
if (errorMap) return { errorMap: errorMap, description };
const customMap: ZodErrorMap = (iss, ctx) => {
const { message } = params;

if (iss.code === "invalid_enum_value") {
return { message: message ?? ctx.defaultError };
}
if (typeof ctx.data === "undefined") {
return { message: message ?? required_error ?? ctx.defaultError };
}
if (iss.code !== "invalid_type") return { message: ctx.defaultError };
return { message: message ?? invalid_type_error ?? ctx.defaultError };
};
return { errorMap: customMap, description };
}
1 change: 1 addition & 0 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ test("should handle server cancelling a request", async () => {
// Request should be rejected
await expect(createMessagePromise).rejects.toBe("Cancelled by test");
});

test("should handle request timeout", async () => {
const server = new Server(
{
Expand Down
24 changes: 20 additions & 4 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
mergeCapabilities,
Protocol,
ProtocolOptions,
RequestOptions,
Expand Down Expand Up @@ -32,7 +33,7 @@ export type ServerOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this server.
*/
capabilities: ServerCapabilities;
capabilities?: ServerCapabilities;

/**
* Optional instructions describing how to use the server and its features.
Expand Down Expand Up @@ -89,11 +90,11 @@ export class Server<
*/
constructor(
private _serverInfo: Implementation,
options: ServerOptions,
options?: ServerOptions,
) {
super(options);
this._capabilities = options.capabilities;
this._instructions = options.instructions;
this._capabilities = options?.capabilities ?? {};
this._instructions = options?.instructions;

this.setRequestHandler(InitializeRequestSchema, (request) =>
this._oninitialize(request),
Expand All @@ -103,6 +104,21 @@ export class Server<
);
}

/**
* Registers new capabilities. This can only be called before connecting to a transport.
*
* The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
*/
public registerCapabilities(capabilities: ServerCapabilities): void {
if (this.transport) {
throw new Error(
"Cannot register capabilities after connecting to transport",
);
}

this._capabilities = mergeCapabilities(this._capabilities, capabilities);
}

protected assertCapabilityForMethod(method: RequestT["method"]): void {
switch (method as ServerRequest["method"]) {
case "sampling/createMessage":
Expand Down
Loading
Loading