Skip to content
Closed
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
7 changes: 7 additions & 0 deletions src/server/completable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/**
* Checks if a schema is completable (has completion metadata).
*/
export function isCompletable(schema: unknown): schema is CompletableSchema<AnySchema> {

Check failure on line 37 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Duplicate function implementation.

Check failure on line 37 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot redeclare exported variable 'isCompletable'.
return !!schema && typeof schema === 'object' && COMPLETABLE_SYMBOL in (schema as object);
}

Expand All @@ -46,6 +46,13 @@
return meta?.complete as CompleteCallback<T> | undefined;
}

// Runtime type guard to detect Completable-wrapped Zod types across versions
export function isCompletable<T extends ZodTypeAny = ZodTypeAny>(value: unknown): value is Completable<T> {

Check failure on line 50 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot find name 'Completable'. Did you mean 'CompletableDef'?

Check failure on line 50 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot find name 'ZodTypeAny'.

Check failure on line 50 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot find name 'ZodTypeAny'.

Check failure on line 50 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Duplicate function implementation.

Check failure on line 50 in src/server/completable.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot redeclare exported variable 'isCompletable'.
if (value === null || typeof value !== 'object') return false;
const obj = value as { _def?: { typeName?: unknown } };
return obj._def?.typeName === McpZodTypeKind.Completable;
}

/**
* Unwraps a completable schema to get the underlying schema.
* For backward compatibility with code that called `.unwrap()`.
Expand Down
112 changes: 100 additions & 12 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4752,9 +4752,53 @@
};
});

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
/***
* Test: Registering a resource template without a complete callback should not update server capabilities to advertise support for completion
*/
test('should not advertise support for completion when a resource template without a complete callback is defined', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});

mcpServer.resource(
'test',
new ResourceTemplate('test://resource/{category}', {
list: undefined
}),
async () => ({
contents: [
{
uri: 'test://resource/test',
text: 'Test content'
}
]
})
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);

expect(client.getServerCapabilities()).not.toHaveProperty('completions');
});

/***
* Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion
*/
test('should advertise support for completion when a resource template with a complete callback is defined', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});

const invalidTypeResult = await client.callTool({
name: 'union-test',
Expand Down Expand Up @@ -5800,14 +5844,57 @@
}
});

expect(checkAvailability).toHaveBeenCalledWith('ABC Restaurant', '2024-12-25', 2);
expect(findAlternatives).not.toHaveBeenCalled();
expect(result.content).toEqual([
{
type: 'text',
text: 'No booking made. Original date not available.'
}
]);
/***
* Test: Registering a prompt without a completable argument should not update server capabilities to advertise support for completion
*/
test('should not advertise support for completion when a prompt without a completable argument is defined', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});

mcpServer.prompt(
'test-prompt',
{
name: z.string()
},
async ({ name }) => ({
messages: [
{
role: 'assistant',
content: {
type: 'text',
text: `Hello ${name}`
}
}
]
})
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);

const capabilities = client.getServerCapabilities() || {};
const keys = Object.keys(capabilities);
expect(keys).not.toContain('completions');
});

/***
* Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion
*/
test('should advertise support for completion when a prompt with a completable argument is defined', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});

test('should handle user cancelling the elicitation', async () => {
Expand All @@ -5823,7 +5910,8 @@

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
expect(client.getServerCapabilities()).toMatchObject({ completions: {}, prompts: { listChanged: true } });
});

// Call the tool
const result = await client.callTool({
Expand Down Expand Up @@ -6683,3 +6771,3 @@
});
});
});
31 changes: 23 additions & 8 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
CallToolRequest,
ToolExecution
} from '../types.js';
import { isCompletable, getCompleter } from './completable.js';
import { CompletableDef, isCompletable } from './completable.js';
import { UriTemplate, Variables } from '../shared/uriTemplate.js';
import { RequestHandlerExtra } from '../shared/protocol.js';
import { Transport } from '../shared/transport.js';
Expand Down Expand Up @@ -441,13 +441,12 @@
return EMPTY_COMPLETION_RESULT;
}

const promptShape = getObjectShape(prompt.argsSchema);
const field = promptShape?.[request.params.argument.name];
if (!isCompletable(field)) {
const field = prompt.argsSchema.shape[request.params.argument.name];

Check failure on line 444 in src/server/mcp.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Property 'shape' does not exist on type 'AnyObjectSchema'.
if (!isCompletable<ZodString>(field)) {

Check failure on line 445 in src/server/mcp.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot find name 'ZodString'. Did you mean 'String'?
return EMPTY_COMPLETION_RESULT;
}

const completer = getCompleter(field);

Check failure on line 449 in src/server/mcp.ts

View workflow job for this annotation

GitHub Actions / pkg-publish

Cannot find name 'getCompleter'. Did you mean 'completer'?
if (!completer) {
return EMPTY_COMPLETION_RESULT;
}
Expand Down Expand Up @@ -557,8 +556,6 @@
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
});

this.setCompletionRequestHandler();

this._resourceHandlersInitialized = true;
}

Expand Down Expand Up @@ -623,8 +620,6 @@
}
});

this.setCompletionRequestHandler();

this._promptHandlersInitialized = true;
}

Expand Down Expand Up @@ -815,6 +810,14 @@
}
};
this._registeredResourceTemplates[name] = registeredResourceTemplate;

// If the resource template has any completion callbacks, enable completions capability
const variableNames = template.uriTemplate.variableNames;
const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v));
if (hasCompleter) {
this.setCompletionRequestHandler();
}

return registeredResourceTemplate;
}

Expand Down Expand Up @@ -848,6 +851,18 @@
}
};
this._registeredPrompts[name] = registeredPrompt;

// If any argument uses a Completable schema, enable completions capability
if (argsSchema) {
const hasCompletable = Object.values(argsSchema).some(field => {
const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field;
return isCompletable(inner);
});
if (hasCompletable) {
this.setCompletionRequestHandler();
}
}

return registeredPrompt;
}

Expand Down
Loading