Skip to content
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

Fix typing for custom requests, notification, and result types #33

Merged
merged 7 commits into from
Nov 5, 2024
Merged
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
78 changes: 78 additions & 0 deletions src/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Client } from "./index.js";
import { z } from "zod";
import { RequestSchema, NotificationSchema, ResultSchema } from "../types.js";

/*
Test that custom request/notification/result schemas can be used with the Client class.
*/
test("should typecheck", () => {
const GetWeatherRequestSchema = RequestSchema.extend({
method: z.literal("weather/get"),
params: z.object({
city: z.string(),
}),
});

const GetForecastRequestSchema = RequestSchema.extend({
method: z.literal("weather/forecast"),
params: z.object({
city: z.string(),
days: z.number(),
}),
});

const WeatherForecastNotificationSchema = NotificationSchema.extend({
method: z.literal("weather/alert"),
params: z.object({
severity: z.enum(["warning", "watch"]),
message: z.string(),
}),
});

const WeatherRequestSchema = GetWeatherRequestSchema.or(
GetForecastRequestSchema,
);
const WeatherNotificationSchema = WeatherForecastNotificationSchema;
const WeatherResultSchema = ResultSchema.extend({
temperature: z.number(),
conditions: z.string(),
});

type WeatherRequest = z.infer<typeof WeatherRequestSchema>;
type WeatherNotification = z.infer<typeof WeatherNotificationSchema>;
type WeatherResult = z.infer<typeof WeatherResultSchema>;

// Create a typed Client for weather data
const weatherClient = new Client<
WeatherRequest,
WeatherNotification,
WeatherResult
>({
name: "WeatherClient",
version: "1.0.0",
});

// Typecheck that only valid weather requests/notifications/results are allowed
false &&
weatherClient.request(
{
method: "weather/get",
params: {
city: "Seattle",
},
},
WeatherResultSchema,
);

false &&
weatherClient.notification({
method: "weather/alert",
params: {
severity: "warning",
message: "Storm approaching",
},
});
});
35 changes: 31 additions & 4 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,46 @@ import {
ClientResult,
Implementation,
InitializeResultSchema,
Notification,
PROTOCOL_VERSION,
Request,
Result,
ServerCapabilities,
} from "../types.js";

/**
* An MCP client on top of a pluggable transport.
*
* The client will automatically begin the initialization flow with the server when connect() is called.
*
* To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters:
*
* ```typescript
* // Custom schemas
* const CustomRequestSchema = RequestSchema.extend({...})
* const CustomNotificationSchema = NotificationSchema.extend({...})
* const CustomResultSchema = ResultSchema.extend({...})
*
* // Type aliases
* type CustomRequest = z.infer<typeof CustomRequestSchema>
* type CustomNotification = z.infer<typeof CustomNotificationSchema>
* type CustomResult = z.infer<typeof CustomResultSchema>
*
* // Create typed client
* const client = new Client<CustomRequest, CustomNotification, CustomResult>({
* name: "CustomClient",
* version: "1.0.0"
* })
* ```
*/
export class Client extends Protocol<
ClientRequest,
ClientNotification,
ClientResult
export class Client<
RequestT extends Request = Request,
NotificationT extends Notification = Notification,
ResultT extends Result = Result,
> extends Protocol<
ClientRequest | RequestT,
ClientNotification | NotificationT,
ClientResult | ResultT
> {
private _serverCapabilities?: ServerCapabilities;
private _serverVersion?: Implementation;
Expand Down
72 changes: 72 additions & 0 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Server } from "./index.js";
import { z } from "zod";
import { RequestSchema, NotificationSchema, ResultSchema } from "../types.js";

/*
Test that custom request/notification/result schemas can be used with the Server class.
*/
test("should typecheck", () => {
const GetWeatherRequestSchema = RequestSchema.extend({
method: z.literal("weather/get"),
params: z.object({
city: z.string(),
}),
});

const GetForecastRequestSchema = RequestSchema.extend({
method: z.literal("weather/forecast"),
params: z.object({
city: z.string(),
days: z.number(),
}),
});

const WeatherForecastNotificationSchema = NotificationSchema.extend({
method: z.literal("weather/alert"),
params: z.object({
severity: z.enum(["warning", "watch"]),
message: z.string(),
}),
});

const WeatherRequestSchema = GetWeatherRequestSchema.or(
GetForecastRequestSchema,
);
const WeatherNotificationSchema = WeatherForecastNotificationSchema;
const WeatherResultSchema = ResultSchema.extend({
temperature: z.number(),
conditions: z.string(),
});

type WeatherRequest = z.infer<typeof WeatherRequestSchema>;
type WeatherNotification = z.infer<typeof WeatherNotificationSchema>;
type WeatherResult = z.infer<typeof WeatherResultSchema>;

// Create a typed Server for weather data
const weatherServer = new Server<
WeatherRequest,
WeatherNotification,
WeatherResult
>({
name: "WeatherServer",
version: "1.0.0",
});

// Typecheck that only valid weather requests/notifications/results are allowed
weatherServer.setRequestHandler(GetWeatherRequestSchema, (request) => {
return {
temperature: 72,
conditions: "sunny",
};
});

weatherServer.setNotificationHandler(
WeatherForecastNotificationSchema,
(notification) => {
console.log(`Weather alert: ${notification.params.message}`);
},
);
});
35 changes: 31 additions & 4 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
InitializeRequest,
InitializeRequestSchema,
InitializeResult,
Notification,
PROTOCOL_VERSION,
Request,
Result,
ServerNotification,
ServerRequest,
ServerResult,
Expand All @@ -21,11 +24,35 @@ import {
* An MCP server on top of a pluggable transport.
*
* This server will automatically respond to the initialization flow as initiated from the client.
*
* To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters:
*
* ```typescript
* // Custom schemas
* const CustomRequestSchema = RequestSchema.extend({...})
* const CustomNotificationSchema = NotificationSchema.extend({...})
* const CustomResultSchema = ResultSchema.extend({...})
*
* // Type aliases
* type CustomRequest = z.infer<typeof CustomRequestSchema>
* type CustomNotification = z.infer<typeof CustomNotificationSchema>
* type CustomResult = z.infer<typeof CustomResultSchema>
*
* // Create typed server
* const server = new Server<CustomRequest, CustomNotification, CustomResult>({
* name: "CustomServer",
* version: "1.0.0"
* })
* ```
*/
export class Server extends Protocol<
ServerRequest,
ServerNotification,
ServerResult
export class Server<
RequestT extends Request = Request,
NotificationT extends Notification = Notification,
ResultT extends Result = Result,
> extends Protocol<
ServerRequest | RequestT,
ServerNotification | NotificationT,
ServerResult | ResultT
> {
private _clientCapabilities?: ClientCapabilities;
private _clientVersion?: Implementation;
Expand Down