|
1 | 1 | import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; |
| 2 | +import type { ClientCapabilities, Implementation, ServerContext } from '@modelcontextprotocol/core'; |
2 | 3 | import { |
| 4 | + DEFAULT_NEGOTIATED_PROTOCOL_VERSION, |
3 | 5 | InitializeResultSchema, |
4 | 6 | InMemoryTransport, |
5 | 7 | isJSONRPCResultResponse, |
@@ -130,4 +132,117 @@ describe('Server', () => { |
130 | 132 | await server.close(); |
131 | 133 | }); |
132 | 134 | }); |
| 135 | + |
| 136 | + describe('ctx.client / ctx.mcpReq.protocolVersion on the handler context', () => { |
| 137 | + /** |
| 138 | + * Connects the server, registers a ping handler that captures its ServerContext, drives the |
| 139 | + * initialize handshake (with the given client capabilities/info), then sends a ping so the |
| 140 | + * handler runs. Returns the captured context. |
| 141 | + */ |
| 142 | + async function captureContextAfterInitialize( |
| 143 | + server: Server, |
| 144 | + requestedVersion: string, |
| 145 | + clientCapabilities: ClientCapabilities, |
| 146 | + clientInfo: Implementation |
| 147 | + ): Promise<ServerContext> { |
| 148 | + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 149 | + await server.connect(serverTransport); |
| 150 | + |
| 151 | + let captured: ServerContext | undefined; |
| 152 | + server.setRequestHandler('ping', async (_request, ctx) => { |
| 153 | + captured = ctx; |
| 154 | + return {}; |
| 155 | + }); |
| 156 | + |
| 157 | + await clientTransport.start(); |
| 158 | + |
| 159 | + const initResponse = new Promise<void>(resolve => { |
| 160 | + clientTransport.onmessage = () => resolve(); |
| 161 | + }); |
| 162 | + await clientTransport.send({ |
| 163 | + jsonrpc: '2.0', |
| 164 | + id: 1, |
| 165 | + method: 'initialize', |
| 166 | + params: { protocolVersion: requestedVersion, capabilities: clientCapabilities, clientInfo } |
| 167 | + } as JSONRPCMessage); |
| 168 | + await initResponse; |
| 169 | + |
| 170 | + const pingResponse = new Promise<void>(resolve => { |
| 171 | + clientTransport.onmessage = () => resolve(); |
| 172 | + }); |
| 173 | + await clientTransport.send({ jsonrpc: '2.0', id: 2, method: 'ping', params: {} } as JSONRPCMessage); |
| 174 | + await pingResponse; |
| 175 | + |
| 176 | + if (!captured) { |
| 177 | + throw new Error('ping handler did not run'); |
| 178 | + } |
| 179 | + return captured; |
| 180 | + } |
| 181 | + |
| 182 | + it('exposes the client capabilities and info the client declared at initialize', async () => { |
| 183 | + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} }); |
| 184 | + const clientCapabilities: ClientCapabilities = { sampling: {}, roots: { listChanged: true } }; |
| 185 | + const clientInfo: Implementation = { name: 'declaring-client', version: '2.3.4' }; |
| 186 | + |
| 187 | + const ctx = await captureContextAfterInitialize(server, LATEST_PROTOCOL_VERSION, clientCapabilities, clientInfo); |
| 188 | + |
| 189 | + // The declared capabilities round-trip onto the context. |
| 190 | + expect(ctx.client.capabilities.sampling).toEqual({}); |
| 191 | + expect(ctx.client.capabilities.roots).toEqual({ listChanged: true }); |
| 192 | + expect(ctx.client.info).toEqual(clientInfo); |
| 193 | + // The per-request facts match the connection-scoped getters (same source of truth). |
| 194 | + expect(ctx.client.capabilities).toEqual(server.getClientCapabilities()); |
| 195 | + expect(ctx.client.info).toEqual(server.getClientVersion()); |
| 196 | + |
| 197 | + await server.close(); |
| 198 | + }); |
| 199 | + |
| 200 | + it('exposes the negotiated protocol version, including a pinned older version', async () => { |
| 201 | + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} }); |
| 202 | + |
| 203 | + const ctx = await captureContextAfterInitialize(server, OLDER_SUPPORTED_VERSION, {}, { name: 'c', version: '1.0.0' }); |
| 204 | + |
| 205 | + expect(ctx.mcpReq.protocolVersion).toBe(OLDER_SUPPORTED_VERSION); |
| 206 | + expect(ctx.mcpReq.protocolVersion).toBe(server.getNegotiatedProtocolVersion()); |
| 207 | + |
| 208 | + await server.close(); |
| 209 | + }); |
| 210 | + |
| 211 | + it('yields {}-shaped capabilities (not undefined) when the client declares no optional capabilities', async () => { |
| 212 | + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} }); |
| 213 | + |
| 214 | + const ctx = await captureContextAfterInitialize(server, LATEST_PROTOCOL_VERSION, {}, { name: 'c', version: '1.0.0' }); |
| 215 | + |
| 216 | + expect(ctx.client.capabilities).toEqual({}); |
| 217 | + expect(ctx.client.info).toEqual({ name: 'c', version: '1.0.0' }); |
| 218 | + |
| 219 | + await server.close(); |
| 220 | + }); |
| 221 | + |
| 222 | + it('pre-initialize: ping before the handshake gets {} capabilities, undefined info, and the default version', async () => { |
| 223 | + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} }); |
| 224 | + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 225 | + await server.connect(serverTransport); |
| 226 | + |
| 227 | + let captured: ServerContext | undefined; |
| 228 | + server.setRequestHandler('ping', async (_request, ctx) => { |
| 229 | + captured = ctx; |
| 230 | + return {}; |
| 231 | + }); |
| 232 | + |
| 233 | + await clientTransport.start(); |
| 234 | + const pingResponse = new Promise<void>(resolve => { |
| 235 | + clientTransport.onmessage = () => resolve(); |
| 236 | + }); |
| 237 | + // No initialize handshake first - only ping is legal pre-initialize. |
| 238 | + await clientTransport.send({ jsonrpc: '2.0', id: 1, method: 'ping', params: {} } as JSONRPCMessage); |
| 239 | + await pingResponse; |
| 240 | + |
| 241 | + expect(captured?.client.capabilities).toEqual({}); |
| 242 | + expect(captured?.client.info).toBeUndefined(); |
| 243 | + expect(captured?.mcpReq.protocolVersion).toBe(DEFAULT_NEGOTIATED_PROTOCOL_VERSION); |
| 244 | + |
| 245 | + await server.close(); |
| 246 | + }); |
| 247 | + }); |
133 | 248 | }); |
0 commit comments