Improve list command signature formatting
This commit is contained in:
parent
a3fdcba097
commit
be79738b16
@ -11,25 +11,28 @@ Both forms share the same validation pipeline, so required parameters, enums, an
|
||||
|
||||
## Reading the CLI Signatures
|
||||
|
||||
`mcporter list <server>` now prints each tool like a mini TypeScript snippet:
|
||||
`mcporter list <server>` prints each tool as a compact TypeScript declaration:
|
||||
|
||||
```ts
|
||||
// Create a comment on a specific Linear issue
|
||||
create_comment({
|
||||
issueId: string // The issue ID
|
||||
body: string // The content of the comment as Markdown
|
||||
parentId?: string // A parent comment ID to reply to
|
||||
})
|
||||
-> result: array // List of calculation results (falls back to `unknown` when unspecified)
|
||||
-> total: number // Total results returned
|
||||
/**
|
||||
* Create a comment on a specific Linear issue.
|
||||
* @param issueId The issue ID
|
||||
* @param body The content of the comment as Markdown
|
||||
* @param parentId? A parent comment ID to reply to
|
||||
*/
|
||||
function create_comment(issueId: string, body: string, parentId?: string): Comment;
|
||||
// optional (3): notifySubscribers, labelIds, mentionIds, ...
|
||||
```
|
||||
|
||||
- Required parameters appear without `?`, optional parameters use `?`.
|
||||
- Literal unions (enums) render as `"json" | "markdown"`.
|
||||
- Known formats (e.g. ISO 8601) surface inline: `dueDate?: string /* ISO 8601 */`.
|
||||
- Each parameter’s schema description is shown as a dimmed `//` comment to match the CLI styling.
|
||||
- Return values are summarised with `->` lines derived from the tool’s output schema; when no schema is available we emit `-> result: unknown` as a fallback.
|
||||
- After the tool list you’ll see an `Examples:` block with a few ready-to-run calls; the legacy flag form is still accepted but no longer printed for every tool.
|
||||
Key details:
|
||||
|
||||
- Doc blocks use `@param` lines so every parameter description (even optional ones) stays in view.
|
||||
- Required parameters appear without `?`; optional parameters use `?` and inherit enum literals (e.g. `"json" | "markdown"`).
|
||||
- Known format hints are appended inline: `dueDate?: string /* ISO 8601 */` (we suppress the hint when the description already calls it out).
|
||||
- When a tool exposes more than two optional parameters (or has ≥4 required parameters), the default output hides the extras and replaces them with an inline summary like `// optional (8): limit, before, after, orderBy, projectId, ...`.
|
||||
- Run `mcporter list <server> --include-optional` whenever you want the full signature; the footer repeats `Optional parameters hidden; run with --include-optional to view all fields.` any time truncation occurs.
|
||||
- Return types come from each tool’s output schema, so you’ll see concrete names when providers include `title` metadata (e.g. `DocumentConnection`). When no schema is advertised we omit the `: Type` suffix entirely instead of showing `unknown`.
|
||||
- Each server concludes with a short `Examples:` block that mirrors the preferred function-call syntax.
|
||||
|
||||
## Function-Call Syntax Details
|
||||
|
||||
|
||||
@ -364,164 +364,117 @@ function printToolDetail(
|
||||
requiredOnly: boolean
|
||||
): ToolDetailResult {
|
||||
const options = extractOptions(tool as ServerToolInfo);
|
||||
const visibleOptions = requiredOnly ? options.filter((entry) => entry.required) : options;
|
||||
const lines = formatToolSignatureBlock(tool.name, tool.description ?? '', visibleOptions, options, requiredOnly);
|
||||
for (const line of lines) {
|
||||
console.log(` ${line}`);
|
||||
const { displayOptions, hiddenOptions } = selectDisplayOptions(options, requiredOnly);
|
||||
const docLines = buildDocComment(tool.description, options);
|
||||
if (docLines) {
|
||||
for (const line of docLines) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
console.log(` ${formatFunctionSignature(tool.name, displayOptions, tool.outputSchema)}`);
|
||||
if (hiddenOptions.length > 0 && requiredOnly) {
|
||||
console.log(` ${formatOptionalSummary(hiddenOptions)}`);
|
||||
}
|
||||
|
||||
if (includeSchema && tool.inputSchema) {
|
||||
// Schemas can be large — indenting keeps multi-line JSON legible without disrupting surrounding output.
|
||||
console.log(indent(JSON.stringify(tool.inputSchema, null, 2), ' '));
|
||||
}
|
||||
const returnLines = formatReturnLines(tool.outputSchema);
|
||||
if (returnLines && returnLines.length > 0) {
|
||||
for (const line of returnLines) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
return {
|
||||
example: formatCallExpressionExample(serverName, tool.name, visibleOptions.length > 0 ? visibleOptions : options),
|
||||
optionalOmitted: requiredOnly && options.length > visibleOptions.length,
|
||||
example: formatCallExpressionExample(serverName, tool.name, displayOptions.length > 0 ? displayOptions : options),
|
||||
optionalOmitted: hiddenOptions.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
function formatToolSignatureBlock(
|
||||
name: string,
|
||||
description: string,
|
||||
visibleOptions: GeneratedOption[],
|
||||
allOptions: GeneratedOption[],
|
||||
|
||||
function selectDisplayOptions(
|
||||
options: GeneratedOption[],
|
||||
requiredOnly: boolean
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
if (description) {
|
||||
lines.push(extraDimText(`// ${description}`));
|
||||
): { displayOptions: GeneratedOption[]; hiddenOptions: GeneratedOption[] } {
|
||||
if (!requiredOnly) {
|
||||
return { displayOptions: options, hiddenOptions: [] };
|
||||
}
|
||||
const omittedOptions = requiredOnly ? allOptions.filter((entry) => !entry.required) : [];
|
||||
const optionalNote = formatOptionalNote(omittedOptions);
|
||||
|
||||
const inlineEligible = isInlineFriendly(visibleOptions, optionalNote);
|
||||
|
||||
if (inlineEligible) {
|
||||
const signature = buildInlineSignature(name, visibleOptions);
|
||||
lines.push(optionalNote ? `${signature} ${optionalNote}` : signature);
|
||||
return lines;
|
||||
const requiredCount = options.filter((option) => option.required).length;
|
||||
const optionalCount = options.length - requiredCount;
|
||||
const includeOptional = optionalCount > 0 && optionalCount <= 2 && requiredCount < 4;
|
||||
const displayOptions: GeneratedOption[] = [];
|
||||
const hiddenOptions: GeneratedOption[] = [];
|
||||
for (const option of options) {
|
||||
if (option.required || includeOptional) {
|
||||
displayOptions.push(option);
|
||||
} else {
|
||||
hiddenOptions.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleOptions.length === 0) {
|
||||
const signature = requiredOnly && allOptions.length > 0 ? `${cyanText(name)}({})` : `${cyanText(name)}()`;
|
||||
lines.push(optionalNote ? `${signature} ${optionalNote}` : signature);
|
||||
return lines;
|
||||
}
|
||||
|
||||
lines.push(`${cyanText(name)}({`);
|
||||
for (const option of visibleOptions) {
|
||||
lines.push(` ${formatParameterSignature(option)}`);
|
||||
}
|
||||
const closing = optionalNote ? `}) ${optionalNote}` : '})';
|
||||
lines.push(closing);
|
||||
return lines;
|
||||
return { displayOptions, hiddenOptions };
|
||||
}
|
||||
|
||||
function isInlineFriendly(options: GeneratedOption[], optionalNote: string | undefined): boolean {
|
||||
if (options.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (options.length > 2) {
|
||||
return false;
|
||||
}
|
||||
return options.every((option) => {
|
||||
const commentLength = option.description?.length ?? 0;
|
||||
return commentsFitsInline(commentLength) && !option.enumValues && option.type !== 'array';
|
||||
});
|
||||
}
|
||||
|
||||
function commentsFitsInline(length: number, max = 60): boolean {
|
||||
return length <= max;
|
||||
}
|
||||
|
||||
function buildInlineSignature(name: string, options: GeneratedOption[]): string {
|
||||
if (options.length === 0) {
|
||||
return `${cyanText(name)}()`;
|
||||
}
|
||||
const parts = options.map((option) => {
|
||||
const typeAnnotation = formatTypeAnnotation(option);
|
||||
const optionalSuffix = option.required ? '' : '?';
|
||||
const commentSuffix = option.description ? ` ${extraDimText(`// ${option.description}`)}` : '';
|
||||
return `${option.property}${optionalSuffix}: ${typeAnnotation}${commentSuffix}`;
|
||||
});
|
||||
if (options.length === 1) {
|
||||
return `${cyanText(name)}(${parts[0]})`;
|
||||
}
|
||||
return `${cyanText(name)}({ ${parts.join(', ')} })`;
|
||||
}
|
||||
|
||||
function formatOptionalNote(omittedOptions: GeneratedOption[], includeAll: boolean): string | undefined {
|
||||
if (omittedOptions.length === 0) {
|
||||
function buildDocComment(description: string | undefined, options: GeneratedOption[]): string[] | undefined {
|
||||
const descriptionLines = description?.trim().split(/\r?\n/) ?? [];
|
||||
const paramDocs = options.filter((option) => option.description);
|
||||
if (descriptionLines.length === 0 && paramDocs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (includeAll) {
|
||||
return undefined;
|
||||
const lines: string[] = ['/**'];
|
||||
for (const line of descriptionLines) {
|
||||
if (line.trim().length > 0) {
|
||||
lines.push(` * ${line.trimEnd()}`);
|
||||
}
|
||||
}
|
||||
const names = omittedOptions.map((option) => option.property);
|
||||
const truncated = names.length > 5 ? [...names.slice(0, 5), '…'] : names;
|
||||
return extraDimText(`// optional (${names.length}): ${truncated.join(', ')}`);
|
||||
for (const option of paramDocs) {
|
||||
const descriptionLines = option.description?.split(/\r?\n/) ?? [''];
|
||||
descriptionLines.forEach((entry, index) => {
|
||||
const suffix = entry.trimEnd();
|
||||
if (index === 0) {
|
||||
lines.push(` * @param ${option.property}${option.required ? '' : '?'} ${suffix}`);
|
||||
return;
|
||||
}
|
||||
if (suffix.length > 0) {
|
||||
lines.push(` * ${suffix}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
lines.push(' */');
|
||||
return lines.map((line) => extraDimText(line));
|
||||
}
|
||||
|
||||
function formatReturnLines(schema: unknown): string[] | undefined {
|
||||
function formatFunctionSignature(name: string, options: GeneratedOption[], outputSchema: unknown): string {
|
||||
const paramsText = options.map(formatInlineParameter).join(', ');
|
||||
const returnType = inferReturnTypeName(outputSchema);
|
||||
const signature = `${cyanText(`function ${name}`)}(${paramsText})`;
|
||||
return returnType ? `${signature}: ${returnType};` : `${signature};`;
|
||||
}
|
||||
|
||||
function formatInlineParameter(option: GeneratedOption): string {
|
||||
const typeAnnotation = formatTypeAnnotation(option);
|
||||
const optionalSuffix = option.required ? '' : '?';
|
||||
return `${option.property}${optionalSuffix}: ${typeAnnotation}`;
|
||||
}
|
||||
|
||||
function formatOptionalSummary(hiddenOptions: GeneratedOption[]): string {
|
||||
const maxNames = 5;
|
||||
const names = hiddenOptions.map((option) => option.property);
|
||||
if (names.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const preview = names.slice(0, maxNames).join(', ');
|
||||
const suffix = names.length > maxNames ? ', ...' : '';
|
||||
return extraDimText(`// optional (${names.length}): ${preview}${suffix}`);
|
||||
}
|
||||
|
||||
function inferReturnTypeName(schema: unknown): string | undefined {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const record = schema as Record<string, unknown>;
|
||||
const type = typeof record.type === 'string' ? (record.type as string) : undefined;
|
||||
|
||||
if (type === 'object' || (!type && typeof record.properties === 'object')) {
|
||||
const properties = (record.properties ?? {}) as Record<string, unknown>;
|
||||
const entries = Object.entries(properties);
|
||||
if (entries.length === 0) {
|
||||
return ['-> result: object'];
|
||||
}
|
||||
const lines: string[] = [];
|
||||
const limit = 5;
|
||||
entries.slice(0, limit).forEach(([key, descriptor]) => {
|
||||
if (!descriptor || typeof descriptor !== 'object') {
|
||||
lines.push(formatReturnEntry(key, 'unknown'));
|
||||
return;
|
||||
}
|
||||
const descRecord = descriptor as Record<string, unknown>;
|
||||
const descType = inferSchemaDisplayType(descRecord);
|
||||
const description = typeof descRecord.description === 'string' ? (descRecord.description as string) : undefined;
|
||||
lines.push(formatReturnEntry(key, descType, description));
|
||||
});
|
||||
if (entries.length > limit) {
|
||||
lines.push(extraDimText(`-> … ${entries.length - limit} more field(s)`));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
const items =
|
||||
record.items && typeof record.items === 'object' ? (record.items as Record<string, unknown>) : undefined;
|
||||
const itemType = items ? inferSchemaDisplayType(items) : 'unknown';
|
||||
const description = items && typeof items.description === 'string' ? (items.description as string) : undefined;
|
||||
return [formatReturnEntry('items[]', itemType, description)];
|
||||
}
|
||||
|
||||
if (type) {
|
||||
return [formatReturnEntry('result', type)];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatReturnEntry(name: string, type: string, description?: string): string {
|
||||
const typeText = dimText(type);
|
||||
const comment = description ? ` ${extraDimText(`// ${description}`)}` : '';
|
||||
return `-> ${name}: ${typeText}${comment}`;
|
||||
return inferSchemaDisplayType(schema as Record<string, unknown>);
|
||||
}
|
||||
|
||||
function inferSchemaDisplayType(descriptor: Record<string, unknown>): string {
|
||||
const title = typeof descriptor.title === 'string' ? descriptor.title.trim() : undefined;
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
const type = typeof descriptor.type === 'string' ? (descriptor.type as string) : undefined;
|
||||
if (!type && typeof descriptor.properties === 'object') {
|
||||
return 'object';
|
||||
@ -529,15 +482,15 @@ function inferSchemaDisplayType(descriptor: Record<string, unknown>): string {
|
||||
if (!type && descriptor.items && typeof descriptor.items === 'object') {
|
||||
return `${inferSchemaDisplayType(descriptor.items as Record<string, unknown>)}[]`;
|
||||
}
|
||||
if (type === 'array' && descriptor.items && typeof descriptor.items === 'object') {
|
||||
return `${inferSchemaDisplayType(descriptor.items as Record<string, unknown>)}[]`;
|
||||
}
|
||||
if (!type && Array.isArray(descriptor.enum)) {
|
||||
const values = (descriptor.enum as unknown[]).filter((entry): entry is string => typeof entry === 'string');
|
||||
if (values.length > 0) {
|
||||
return values.map((entry) => JSON.stringify(entry)).join(' | ');
|
||||
}
|
||||
}
|
||||
if (type === 'array' && descriptor.items && typeof descriptor.items === 'object') {
|
||||
return `${inferSchemaDisplayType(descriptor.items as Record<string, unknown>)}[]`;
|
||||
}
|
||||
return type ?? 'unknown';
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,44 @@ const stripAnsi = (value: string): string => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const linearDefinition: ServerDefinition = {
|
||||
name: 'linear',
|
||||
description: 'Hosted Linear MCP',
|
||||
command: { kind: 'http', url: new URL('https://example.com/mcp') },
|
||||
};
|
||||
|
||||
const buildLinearDocumentsTool = (includeSchema?: boolean) => ({
|
||||
name: 'list_documents',
|
||||
description: "List documents in the user's Linear workspace",
|
||||
inputSchema: includeSchema
|
||||
? {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'The search query' },
|
||||
limit: { type: 'number', description: 'Maximum number of documents to return' },
|
||||
before: { type: 'string', description: 'Cursor to page backwards' },
|
||||
after: { type: 'string', description: 'Cursor to page forwards' },
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
description: 'Sort order for the documents',
|
||||
enum: ['createdAt', 'updatedAt'],
|
||||
},
|
||||
projectId: { type: 'string', description: 'Filter by project' },
|
||||
initiativeId: { type: 'string', description: 'Filter by initiative' },
|
||||
creatorId: { type: 'string', description: 'Filter by creator' },
|
||||
includeArchived: { type: 'boolean', description: 'Whether to include archived documents' },
|
||||
},
|
||||
required: ['query'],
|
||||
}
|
||||
: undefined,
|
||||
outputSchema: includeSchema
|
||||
? {
|
||||
title: 'DocumentConnection',
|
||||
type: 'object',
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
describe('CLI list timeout handling', () => {
|
||||
it('parses --timeout flag into list flags', async () => {
|
||||
const { extractListFlags } = await cliModulePromise;
|
||||
@ -212,20 +250,45 @@ describe('CLI list classification', () => {
|
||||
expect(detailLine).toMatch(/1 tool/);
|
||||
expect(detailLine).toMatch(/ms/);
|
||||
expect(detailLine).toContain('HTTP https://example.com/mcp');
|
||||
expect(lines.some((line) => line.includes('// Add two numbers'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('add(a: number'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('First operand'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('format?:'))).toBe(false);
|
||||
expect(lines.some((line) => line.includes('dueBefore?:'))).toBe(false);
|
||||
expect(lines.some((line) => line.includes('// optional (2): format, dueBefore'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('-> result:'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('-> total:'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('/**'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('@param a') && line.includes('First operand'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('function add('))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('format?: "json" | "markdown"'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('dueBefore?: string'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('// optional'))).toBe(false);
|
||||
expect(lines.some((line) => line.includes('Examples:'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('mcporter call calculator.add(a: 1)'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('mcporter call calculator.add(a: 1'))).toBe(true);
|
||||
expect(
|
||||
lines.some((line) => line.includes('Optional parameters hidden; run with --include-optional to view all fields'))
|
||||
).toBe(false);
|
||||
expect(listToolsSpy).toHaveBeenCalledWith('calculator', { includeSchema: true });
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('summarizes hidden optional parameters and hints include flag', async () => {
|
||||
const { handleList } = await cliModulePromise;
|
||||
const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
|
||||
Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
|
||||
);
|
||||
const runtime = {
|
||||
getDefinition: () => linearDefinition,
|
||||
listTools: listToolsSpy,
|
||||
} as unknown as Awaited<ReturnType<typeof import('../src/runtime.js')['createRuntime']>>;
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await handleList(runtime, ['linear']);
|
||||
|
||||
const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
|
||||
expect(lines.some((line) => line.includes('function list_documents('))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('// optional (8): limit, before, after, orderBy, projectId, ...'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
lines.some((line) => line.includes('Optional parameters hidden; run with --include-optional to view all fields'))
|
||||
).toBe(true);
|
||||
expect(listToolsSpy).toHaveBeenCalledWith('calculator', { includeSchema: true });
|
||||
expect(listToolsSpy).toHaveBeenCalledWith('linear', { includeSchema: true });
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
@ -233,51 +296,34 @@ describe('CLI list classification', () => {
|
||||
it('includes optional parameters when --include-optional is set', async () => {
|
||||
const { handleList } = await cliModulePromise;
|
||||
const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'add',
|
||||
description: 'Add two numbers',
|
||||
inputSchema: options?.includeSchema
|
||||
? {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a: { type: 'number', description: 'First operand' },
|
||||
format: { type: 'string', enum: ['json', 'markdown'], description: 'Output serialization format' },
|
||||
dueBefore: { type: 'string', format: 'date-time', description: 'ISO 8601 timestamp' },
|
||||
},
|
||||
required: ['a'],
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
])
|
||||
Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
|
||||
);
|
||||
const runtime = {
|
||||
getDefinition: (name: string) => ({
|
||||
name,
|
||||
description: 'Test integration server',
|
||||
command: { kind: 'http', url: new URL('https://example.com/mcp') },
|
||||
}),
|
||||
getDefinition: () => linearDefinition,
|
||||
listTools: listToolsSpy,
|
||||
} as unknown as Awaited<ReturnType<typeof import('../src/runtime.js')['createRuntime']>>;
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await handleList(runtime, ['--include-optional', 'calculator']);
|
||||
await handleList(runtime, ['--include-optional', 'linear']);
|
||||
|
||||
const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
|
||||
|
||||
const headerLine = lines.find((line) => line.trim().startsWith('calculator -'));
|
||||
const headerLine = lines.find((line) => line.trim().startsWith('linear -'));
|
||||
expect(headerLine).toBeDefined();
|
||||
const detailLine = lines[lines.indexOf(headerLine as string) + 1] ?? '';
|
||||
expect(detailLine).toMatch(/1 tool/);
|
||||
expect(detailLine).toMatch(/ms/);
|
||||
expect(detailLine).toContain('HTTP https://example.com/mcp');
|
||||
expect(lines.some((line) => line.includes('add({'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('a: number') && line.includes('First operand'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('format?: "json" | "markdown"'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('dueBefore?: string'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('mcporter call calculator.add(a: 1, format: "json")'))).toBe(true);
|
||||
expect(listToolsSpy).toHaveBeenCalledWith('calculator', { includeSchema: true });
|
||||
expect(lines.some((line) => line.includes('/**'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('@param limit?') && line.includes('Maximum number of documents'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(lines.some((line) => line.includes('function list_documents('))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('limit?: number'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('orderBy?: "createdAt" | "updatedAt"'))).toBe(true);
|
||||
expect(lines.some((line) => line.includes('includeArchived?: boolean'))).toBe(true);
|
||||
expect(listToolsSpy).toHaveBeenCalledWith('linear', { includeSchema: true });
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user