@modelcontextprotocol/sdk
Version:
Model Context Protocol implementation for TypeScript
254 lines • 11.4 kB
JavaScript
import { mergeCapabilities, Protocol } from '../shared/protocol.js';
import { CreateMessageResultSchema, ElicitResultSchema, EmptyResultSchema, InitializedNotificationSchema, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, ListRootsResultSchema, McpError, ErrorCode, SUPPORTED_PROTOCOL_VERSIONS, SetLevelRequestSchema, LoggingLevelSchema } from '../types.js';
import Ajv from 'ajv';
/**
* 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 {
/**
* Initializes this server with the given name and version information.
*/
constructor(_serverInfo, options) {
var _a;
super(options);
this._serverInfo = _serverInfo;
// Map log levels by session id
this._loggingLevels = new Map();
// Map LogLevelSchema to severity index
this.LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
// Is a message with the given level ignored in the log level set for the given session id?
this.isMessageIgnored = (level, sessionId) => {
const currentLevel = this._loggingLevels.get(sessionId);
return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false;
};
this._capabilities = (_a = options === null || options === void 0 ? void 0 : options.capabilities) !== null && _a !== void 0 ? _a : {};
this._instructions = options === null || options === void 0 ? void 0 : options.instructions;
this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request));
this.setNotificationHandler(InitializedNotificationSchema, () => { var _a; return (_a = this.oninitialized) === null || _a === void 0 ? void 0 : _a.call(this); });
if (this._capabilities.logging) {
this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => {
var _a;
const transportSessionId = extra.sessionId || ((_a = extra.requestInfo) === null || _a === void 0 ? void 0 : _a.headers['mcp-session-id']) || undefined;
const { level } = request.params;
const parseResult = LoggingLevelSchema.safeParse(level);
if (parseResult.success) {
this._loggingLevels.set(transportSessionId, parseResult.data);
}
return {};
});
}
}
/**
* 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).
*/
registerCapabilities(capabilities) {
if (this.transport) {
throw new Error('Cannot register capabilities after connecting to transport');
}
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
}
assertCapabilityForMethod(method) {
var _a, _b, _c;
switch (method) {
case 'sampling/createMessage':
if (!((_a = this._clientCapabilities) === null || _a === void 0 ? void 0 : _a.sampling)) {
throw new Error(`Client does not support sampling (required for ${method})`);
}
break;
case 'elicitation/create':
if (!((_b = this._clientCapabilities) === null || _b === void 0 ? void 0 : _b.elicitation)) {
throw new Error(`Client does not support elicitation (required for ${method})`);
}
break;
case 'roots/list':
if (!((_c = this._clientCapabilities) === null || _c === void 0 ? void 0 : _c.roots)) {
throw new Error(`Client does not support listing roots (required for ${method})`);
}
break;
case 'ping':
// No specific capability required for ping
break;
}
}
assertNotificationCapability(method) {
switch (method) {
case 'notifications/message':
if (!this._capabilities.logging) {
throw new Error(`Server does not support logging (required for ${method})`);
}
break;
case 'notifications/resources/updated':
case 'notifications/resources/list_changed':
if (!this._capabilities.resources) {
throw new Error(`Server does not support notifying about resources (required for ${method})`);
}
break;
case 'notifications/tools/list_changed':
if (!this._capabilities.tools) {
throw new Error(`Server does not support notifying of tool list changes (required for ${method})`);
}
break;
case 'notifications/prompts/list_changed':
if (!this._capabilities.prompts) {
throw new Error(`Server does not support notifying of prompt list changes (required for ${method})`);
}
break;
case 'notifications/cancelled':
// Cancellation notifications are always allowed
break;
case 'notifications/progress':
// Progress notifications are always allowed
break;
}
}
assertRequestHandlerCapability(method) {
switch (method) {
case 'sampling/createMessage':
if (!this._capabilities.sampling) {
throw new Error(`Server does not support sampling (required for ${method})`);
}
break;
case 'logging/setLevel':
if (!this._capabilities.logging) {
throw new Error(`Server does not support logging (required for ${method})`);
}
break;
case 'prompts/get':
case 'prompts/list':
if (!this._capabilities.prompts) {
throw new Error(`Server does not support prompts (required for ${method})`);
}
break;
case 'resources/list':
case 'resources/templates/list':
case 'resources/read':
if (!this._capabilities.resources) {
throw new Error(`Server does not support resources (required for ${method})`);
}
break;
case 'tools/call':
case 'tools/list':
if (!this._capabilities.tools) {
throw new Error(`Server does not support tools (required for ${method})`);
}
break;
case 'ping':
case 'initialize':
// No specific capability required for these methods
break;
}
}
async _oninitialize(request) {
const requestedVersion = request.params.protocolVersion;
this._clientCapabilities = request.params.capabilities;
this._clientVersion = request.params.clientInfo;
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION;
return {
protocolVersion,
capabilities: this.getCapabilities(),
serverInfo: this._serverInfo,
...(this._instructions && { instructions: this._instructions })
};
}
/**
* After initialization has completed, this will be populated with the client's reported capabilities.
*/
getClientCapabilities() {
return this._clientCapabilities;
}
/**
* After initialization has completed, this will be populated with information about the client's name and version.
*/
getClientVersion() {
return this._clientVersion;
}
getCapabilities() {
return this._capabilities;
}
async ping() {
return this.request({ method: 'ping' }, EmptyResultSchema);
}
async createMessage(params, options) {
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options);
}
async elicitInput(params, options) {
const result = await this.request({ method: 'elicitation/create', params }, ElicitResultSchema, options);
// Validate the response content against the requested schema if action is "accept"
if (result.action === 'accept' && result.content) {
try {
const ajv = new Ajv();
const validate = ajv.compile(params.requestedSchema);
const isValid = validate(result.content);
if (!isValid) {
throw new McpError(ErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error validating elicitation response: ${error}`);
}
}
return result;
}
async listRoots(params, options) {
return this.request({ method: 'roots/list', params }, ListRootsResultSchema, options);
}
/**
* Sends a logging message to the client, if connected.
* Note: You only need to send the parameters object, not the entire JSON RPC message
* @see LoggingMessageNotification
* @param params
* @param sessionId optional for stateless and backward compatibility
*/
async sendLoggingMessage(params, sessionId) {
if (this._capabilities.logging) {
if (!this.isMessageIgnored(params.level, sessionId)) {
return this.notification({ method: 'notifications/message', params });
}
}
}
async sendResourceUpdated(params) {
return this.notification({
method: 'notifications/resources/updated',
params
});
}
async sendResourceListChanged() {
return this.notification({
method: 'notifications/resources/list_changed'
});
}
async sendToolListChanged() {
return this.notification({ method: 'notifications/tools/list_changed' });
}
async sendPromptListChanged() {
return this.notification({ method: 'notifications/prompts/list_changed' });
}
}
//# sourceMappingURL=index.js.map