Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
747 lines
27 KiB
TypeScript
747 lines
27 KiB
TypeScript
import { ZodType, z } from 'zod';
|
|
import { ClientCapabilities, ErrorCode, McpError, Notification, Request, Result, ServerCapabilities } from '../types.js';
|
|
import { Protocol, mergeCapabilities } from './protocol.js';
|
|
import { Transport } from './transport.js';
|
|
import { MockInstance } from 'vitest';
|
|
|
|
// Mock Transport class
|
|
class MockTransport implements Transport {
|
|
onclose?: () => void;
|
|
onerror?: (error: Error) => void;
|
|
onmessage?: (message: unknown) => void;
|
|
|
|
async start(): Promise<void> {}
|
|
async close(): Promise<void> {
|
|
this.onclose?.();
|
|
}
|
|
async send(_message: unknown): Promise<void> {}
|
|
}
|
|
|
|
describe('protocol tests', () => {
|
|
let protocol: Protocol<Request, Notification, Result>;
|
|
let transport: MockTransport;
|
|
let sendSpy: MockInstance;
|
|
|
|
beforeEach(() => {
|
|
transport = new MockTransport();
|
|
sendSpy = vi.spyOn(transport, 'send');
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})();
|
|
});
|
|
|
|
test('should throw a timeout error if the request exceeds the timeout', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
try {
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
await protocol.request(request, mockSchema, {
|
|
timeout: 0
|
|
});
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(McpError);
|
|
if (error instanceof McpError) {
|
|
expect(error.code).toBe(ErrorCode.RequestTimeout);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should invoke onclose when the connection is closed', async () => {
|
|
const oncloseMock = vi.fn();
|
|
protocol.onclose = oncloseMock;
|
|
await protocol.connect(transport);
|
|
await transport.close();
|
|
expect(oncloseMock).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should not overwrite existing hooks when connecting transports', async () => {
|
|
const oncloseMock = vi.fn();
|
|
const onerrorMock = vi.fn();
|
|
const onmessageMock = vi.fn();
|
|
transport.onclose = oncloseMock;
|
|
transport.onerror = onerrorMock;
|
|
transport.onmessage = onmessageMock;
|
|
await protocol.connect(transport);
|
|
transport.onclose();
|
|
transport.onerror(new Error());
|
|
transport.onmessage('');
|
|
expect(oncloseMock).toHaveBeenCalled();
|
|
expect(onerrorMock).toHaveBeenCalled();
|
|
expect(onmessageMock).toHaveBeenCalled();
|
|
});
|
|
|
|
describe('_meta preservation with onprogress', () => {
|
|
test('should preserve existing _meta when adding progressToken', async () => {
|
|
await protocol.connect(transport);
|
|
const request = {
|
|
method: 'example',
|
|
params: {
|
|
data: 'test',
|
|
_meta: {
|
|
customField: 'customValue',
|
|
anotherField: 123
|
|
}
|
|
}
|
|
};
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
|
|
protocol.request(request, mockSchema, {
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'example',
|
|
params: {
|
|
data: 'test',
|
|
_meta: {
|
|
customField: 'customValue',
|
|
anotherField: 123,
|
|
progressToken: expect.any(Number)
|
|
}
|
|
},
|
|
jsonrpc: '2.0',
|
|
id: expect.any(Number)
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
test('should create _meta with progressToken when no _meta exists', async () => {
|
|
await protocol.connect(transport);
|
|
const request = {
|
|
method: 'example',
|
|
params: {
|
|
data: 'test'
|
|
}
|
|
};
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
|
|
protocol.request(request, mockSchema, {
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'example',
|
|
params: {
|
|
data: 'test',
|
|
_meta: {
|
|
progressToken: expect.any(Number)
|
|
}
|
|
},
|
|
jsonrpc: '2.0',
|
|
id: expect.any(Number)
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
test('should not modify _meta when onprogress is not provided', async () => {
|
|
await protocol.connect(transport);
|
|
const request = {
|
|
method: 'example',
|
|
params: {
|
|
data: 'test',
|
|
_meta: {
|
|
customField: 'customValue'
|
|
}
|
|
}
|
|
};
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
|
|
protocol.request(request, mockSchema);
|
|
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'example',
|
|
params: {
|
|
data: 'test',
|
|
_meta: {
|
|
customField: 'customValue'
|
|
}
|
|
},
|
|
jsonrpc: '2.0',
|
|
id: expect.any(Number)
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
test('should handle params being undefined with onprogress', async () => {
|
|
await protocol.connect(transport);
|
|
const request = {
|
|
method: 'example'
|
|
};
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
|
|
protocol.request(request, mockSchema, {
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'example',
|
|
params: {
|
|
_meta: {
|
|
progressToken: expect.any(Number)
|
|
}
|
|
},
|
|
jsonrpc: '2.0',
|
|
id: expect.any(Number)
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('progress notification timeout behavior', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
test('should not reset timeout when resetTimeoutOnProgress is false', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 1000,
|
|
resetTimeoutOnProgress: false,
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
vi.advanceTimersByTime(800);
|
|
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 50,
|
|
total: 100
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
|
|
expect(onProgressMock).toHaveBeenCalledWith({
|
|
progress: 50,
|
|
total: 100
|
|
});
|
|
|
|
vi.advanceTimersByTime(201);
|
|
|
|
await expect(requestPromise).rejects.toThrow('Request timed out');
|
|
});
|
|
|
|
test('should reset timeout when progress notification is received', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 1000,
|
|
resetTimeoutOnProgress: true,
|
|
onprogress: onProgressMock
|
|
});
|
|
vi.advanceTimersByTime(800);
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 50,
|
|
total: 100
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
expect(onProgressMock).toHaveBeenCalledWith({
|
|
progress: 50,
|
|
total: 100
|
|
});
|
|
vi.advanceTimersByTime(800);
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
id: 0,
|
|
result: { result: 'success' }
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
await expect(requestPromise).resolves.toEqual({ result: 'success' });
|
|
});
|
|
|
|
test('should respect maxTotalTimeout', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 1000,
|
|
maxTotalTimeout: 150,
|
|
resetTimeoutOnProgress: true,
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
// First progress notification should work
|
|
vi.advanceTimersByTime(80);
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 50,
|
|
total: 100
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
expect(onProgressMock).toHaveBeenCalledWith({
|
|
progress: 50,
|
|
total: 100
|
|
});
|
|
vi.advanceTimersByTime(80);
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 75,
|
|
total: 100
|
|
}
|
|
});
|
|
}
|
|
await expect(requestPromise).rejects.toThrow('Maximum total timeout exceeded');
|
|
expect(onProgressMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should timeout if no progress received within timeout period', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 100,
|
|
resetTimeoutOnProgress: true
|
|
});
|
|
vi.advanceTimersByTime(101);
|
|
await expect(requestPromise).rejects.toThrow('Request timed out');
|
|
});
|
|
|
|
test('should handle multiple progress notifications correctly', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 1000,
|
|
resetTimeoutOnProgress: true,
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
// Simulate multiple progress updates
|
|
for (let i = 1; i <= 3; i++) {
|
|
vi.advanceTimersByTime(800);
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: i * 25,
|
|
total: 100
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
expect(onProgressMock).toHaveBeenNthCalledWith(i, {
|
|
progress: i * 25,
|
|
total: 100
|
|
});
|
|
}
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
id: 0,
|
|
result: { result: 'success' }
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
await expect(requestPromise).resolves.toEqual({ result: 'success' });
|
|
});
|
|
|
|
test('should handle progress notifications with message field', async () => {
|
|
await protocol.connect(transport);
|
|
const request = { method: 'example', params: {} };
|
|
const mockSchema: ZodType<{ result: string }> = z.object({
|
|
result: z.string()
|
|
});
|
|
const onProgressMock = vi.fn();
|
|
|
|
const requestPromise = protocol.request(request, mockSchema, {
|
|
timeout: 1000,
|
|
onprogress: onProgressMock
|
|
});
|
|
|
|
vi.advanceTimersByTime(200);
|
|
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 25,
|
|
total: 100,
|
|
message: 'Initializing process...'
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
|
|
expect(onProgressMock).toHaveBeenCalledWith({
|
|
progress: 25,
|
|
total: 100,
|
|
message: 'Initializing process...'
|
|
});
|
|
|
|
vi.advanceTimersByTime(200);
|
|
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
method: 'notifications/progress',
|
|
params: {
|
|
progressToken: 0,
|
|
progress: 75,
|
|
total: 100,
|
|
message: 'Processing data...'
|
|
}
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
|
|
expect(onProgressMock).toHaveBeenCalledWith({
|
|
progress: 75,
|
|
total: 100,
|
|
message: 'Processing data...'
|
|
});
|
|
|
|
if (transport.onmessage) {
|
|
transport.onmessage({
|
|
jsonrpc: '2.0',
|
|
id: 0,
|
|
result: { result: 'success' }
|
|
});
|
|
}
|
|
await Promise.resolve();
|
|
await expect(requestPromise).resolves.toEqual({ result: 'success' });
|
|
});
|
|
});
|
|
|
|
describe('Debounced Notifications', () => {
|
|
// We need to flush the microtask queue to test the debouncing logic.
|
|
// This helper function does that.
|
|
const flushMicrotasks = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
it('should NOT debounce a notification that has parameters', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced_with_params'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
// These notifications are configured for debouncing but contain params, so they should be sent immediately.
|
|
await protocol.notification({ method: 'test/debounced_with_params', params: { data: 1 } });
|
|
await protocol.notification({ method: 'test/debounced_with_params', params: { data: 2 } });
|
|
|
|
// ASSERT
|
|
// Both should have been sent immediately to avoid data loss.
|
|
expect(sendSpy).toHaveBeenCalledTimes(2);
|
|
expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 1 } }), undefined);
|
|
expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 2 } }), undefined);
|
|
});
|
|
|
|
it('should NOT debounce a notification that has a relatedRequestId', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced_with_options'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-1' });
|
|
await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-2' });
|
|
|
|
// ASSERT
|
|
expect(sendSpy).toHaveBeenCalledTimes(2);
|
|
expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-1' });
|
|
expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-2' });
|
|
});
|
|
|
|
it('should clear pending debounced notifications on connection close', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
// Schedule a notification but don't flush the microtask queue.
|
|
protocol.notification({ method: 'test/debounced' });
|
|
|
|
// Close the connection. This should clear the pending set.
|
|
await protocol.close();
|
|
|
|
// Now, flush the microtask queue.
|
|
await flushMicrotasks();
|
|
|
|
// ASSERT
|
|
// The send should never have happened because the transport was cleared.
|
|
expect(sendSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should debounce multiple synchronous calls when params property is omitted', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
// This is the more idiomatic way to write a notification with no params.
|
|
protocol.notification({ method: 'test/debounced' });
|
|
protocol.notification({ method: 'test/debounced' });
|
|
protocol.notification({ method: 'test/debounced' });
|
|
|
|
expect(sendSpy).not.toHaveBeenCalled();
|
|
await flushMicrotasks();
|
|
|
|
// ASSERT
|
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
// The final sent object might not even have the `params` key, which is fine.
|
|
// We can check that it was called and that the params are "falsy".
|
|
const sentNotification = sendSpy.mock.calls[0][0];
|
|
expect(sentNotification.method).toBe('test/debounced');
|
|
expect(sentNotification.params).toBeUndefined();
|
|
});
|
|
|
|
it('should debounce calls when params is explicitly undefined', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
protocol.notification({ method: 'test/debounced', params: undefined });
|
|
protocol.notification({ method: 'test/debounced', params: undefined });
|
|
await flushMicrotasks();
|
|
|
|
// ASSERT
|
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'test/debounced',
|
|
params: undefined
|
|
}),
|
|
undefined
|
|
);
|
|
});
|
|
|
|
it('should send non-debounced notifications immediately and multiple times', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced'] }); // Configure for a different method
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
// Call a non-debounced notification method multiple times.
|
|
await protocol.notification({ method: 'test/immediate' });
|
|
await protocol.notification({ method: 'test/immediate' });
|
|
|
|
// ASSERT
|
|
// Since this method is not in the debounce list, it should be sent every time.
|
|
expect(sendSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should not debounce any notifications if the option is not provided', async () => {
|
|
// ARRANGE
|
|
// Use the default protocol from beforeEach, which has no debounce options.
|
|
await protocol.connect(transport);
|
|
|
|
// ACT
|
|
await protocol.notification({ method: 'any/method' });
|
|
await protocol.notification({ method: 'any/method' });
|
|
|
|
// ASSERT
|
|
// Without the config, behavior should be immediate sending.
|
|
expect(sendSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle sequential batches of debounced notifications correctly', async () => {
|
|
// ARRANGE
|
|
protocol = new (class extends Protocol<Request, Notification, Result> {
|
|
protected assertCapabilityForMethod(): void {}
|
|
protected assertNotificationCapability(): void {}
|
|
protected assertRequestHandlerCapability(): void {}
|
|
})({ debouncedNotificationMethods: ['test/debounced'] });
|
|
await protocol.connect(transport);
|
|
|
|
// ACT (Batch 1)
|
|
protocol.notification({ method: 'test/debounced' });
|
|
protocol.notification({ method: 'test/debounced' });
|
|
await flushMicrotasks();
|
|
|
|
// ASSERT (Batch 1)
|
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
|
|
// ACT (Batch 2)
|
|
// After the first batch has been sent, a new batch should be possible.
|
|
protocol.notification({ method: 'test/debounced' });
|
|
protocol.notification({ method: 'test/debounced' });
|
|
await flushMicrotasks();
|
|
|
|
// ASSERT (Batch 2)
|
|
// The total number of sends should now be 2.
|
|
expect(sendSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('mergeCapabilities', () => {
|
|
it('should merge client capabilities', () => {
|
|
const base: ClientCapabilities = {
|
|
sampling: {},
|
|
roots: {
|
|
listChanged: true
|
|
}
|
|
};
|
|
|
|
const additional: ClientCapabilities = {
|
|
experimental: {
|
|
feature: {
|
|
featureFlag: true
|
|
}
|
|
},
|
|
elicitation: {},
|
|
roots: {
|
|
listChanged: true
|
|
}
|
|
};
|
|
|
|
const merged = mergeCapabilities(base, additional);
|
|
expect(merged).toEqual({
|
|
sampling: {},
|
|
elicitation: {},
|
|
roots: {
|
|
listChanged: true
|
|
},
|
|
experimental: {
|
|
feature: {
|
|
featureFlag: true
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should merge server capabilities', () => {
|
|
const base: ServerCapabilities = {
|
|
logging: {},
|
|
prompts: {
|
|
listChanged: true
|
|
}
|
|
};
|
|
|
|
const additional: ServerCapabilities = {
|
|
resources: {
|
|
subscribe: true
|
|
},
|
|
prompts: {
|
|
listChanged: true
|
|
}
|
|
};
|
|
|
|
const merged = mergeCapabilities(base, additional);
|
|
expect(merged).toEqual({
|
|
logging: {},
|
|
prompts: {
|
|
listChanged: true
|
|
},
|
|
resources: {
|
|
subscribe: true
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should override existing values with additional values', () => {
|
|
const base: ServerCapabilities = {
|
|
prompts: {
|
|
listChanged: false
|
|
}
|
|
};
|
|
|
|
const additional: ServerCapabilities = {
|
|
prompts: {
|
|
listChanged: true
|
|
}
|
|
};
|
|
|
|
const merged = mergeCapabilities(base, additional);
|
|
expect(merged.prompts!.listChanged).toBe(true);
|
|
});
|
|
|
|
it('should handle empty objects', () => {
|
|
const base = {};
|
|
const additional = {};
|
|
const merged = mergeCapabilities(base, additional);
|
|
expect(merged).toEqual({});
|
|
});
|
|
});
|