feat: surface server instructions in list
This commit is contained in:
parent
a64bdda3f7
commit
5d8e64d5d5
@ -17,6 +17,7 @@
|
||||
- Let non-interactive `mcporter list` use existing OAuth token caches for HTTP servers even when older configs are missing `auth: "oauth"`. (Issue #137)
|
||||
- Fail OAuth flows immediately when the server never creates an authorization URL, instead of waiting for a browser callback that cannot arrive. (Issue #115)
|
||||
- Support `mcporter list server.tool --schema` to print a single tool's schema instead of the whole server. (Issue #116)
|
||||
- Surface MCP server `instructions` from the initialize response in single-server `mcporter list` text and JSON output. (Issue #76)
|
||||
|
||||
### Config
|
||||
|
||||
|
||||
@ -14,7 +14,8 @@ A quick reference for the primary `mcporter` subcommands. Each command inherits
|
||||
- Without arguments, lists every configured server (with live discovery + brief
|
||||
status).
|
||||
- With a server name, prints TypeScript-style signatures for each tool, doc
|
||||
comments, and optional summaries.
|
||||
comments, optional summaries, and any server `instructions` returned during
|
||||
MCP initialization.
|
||||
- With `server.tool`, prints just that tool; combine with `--schema` for a single
|
||||
tool schema.
|
||||
- Hidden alias: `list-tools` (kept for muscle memory; not advertised in help output).
|
||||
|
||||
@ -273,12 +273,14 @@ export async function handleList(
|
||||
printMissingToolJson(definition, requestedTool, durationMs, transportSummary, flags);
|
||||
return;
|
||||
}
|
||||
const instructions = await loadServerInstructions(runtime, target);
|
||||
const payload = {
|
||||
mode: 'server',
|
||||
name: definition.name,
|
||||
status: 'ok' as StatusCategory,
|
||||
durationMs,
|
||||
description: definition.description,
|
||||
instructions,
|
||||
transport: transportSummary,
|
||||
source: definition.source,
|
||||
sources: flags.verbose || flags.includeSources ? definition.sources : undefined,
|
||||
@ -327,6 +329,7 @@ export async function handleList(
|
||||
printMissingToolText(definition, requestedTool, durationMs, transportSummary, sourcePath);
|
||||
return;
|
||||
}
|
||||
const instructions = await loadServerInstructions(runtime, target);
|
||||
const summaryLine = printSingleServerHeader(
|
||||
definition,
|
||||
metadataEntries.length,
|
||||
@ -335,6 +338,7 @@ export async function handleList(
|
||||
sourcePath,
|
||||
{
|
||||
printSummaryNow: false,
|
||||
instructions,
|
||||
}
|
||||
);
|
||||
if (metadataEntries.length === 0) {
|
||||
@ -528,3 +532,24 @@ function printMissingToolJson(
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function loadServerInstructions(
|
||||
runtime: Awaited<ReturnType<(typeof import('../runtime.js'))['createRuntime']>>,
|
||||
serverName: string
|
||||
): Promise<string | undefined> {
|
||||
if (typeof runtime.connect !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const context = await runtime.connect(serverName);
|
||||
const instructions =
|
||||
typeof context.client.getInstructions === 'function' ? context.client.getInstructions() : undefined;
|
||||
if (typeof instructions !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = instructions.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ export interface ListJsonServerEntry {
|
||||
status: StatusCategory;
|
||||
durationMs: number;
|
||||
description?: string;
|
||||
instructions?: string;
|
||||
transport?: string;
|
||||
source?: ServerDefinition['source'];
|
||||
sources?: ServerDefinition['sources'];
|
||||
@ -38,7 +39,7 @@ export function printSingleServerHeader(
|
||||
durationMs: number | undefined,
|
||||
transportSummary: string,
|
||||
sourcePath: string | undefined,
|
||||
options?: { printSummaryNow?: boolean }
|
||||
options?: { printSummaryNow?: boolean; instructions?: string }
|
||||
): string {
|
||||
const prefix = boldText(definition.name);
|
||||
if (definition.description) {
|
||||
@ -46,6 +47,11 @@ export function printSingleServerHeader(
|
||||
} else {
|
||||
console.log(prefix);
|
||||
}
|
||||
if (options?.instructions) {
|
||||
for (const line of formatInstructionLines(options.instructions)) {
|
||||
console.log(` ${extraDimText(line)}`);
|
||||
}
|
||||
}
|
||||
const summaryParts: string[] = [];
|
||||
summaryParts.push(
|
||||
extraDimText(typeof toolCount === 'number' ? `${toolCount} tool${toolCount === 1 ? '' : 's'}` : 'tools unavailable')
|
||||
@ -69,6 +75,16 @@ export function printSingleServerHeader(
|
||||
return summaryLine;
|
||||
}
|
||||
|
||||
function formatInstructionLines(instructions: string): string[] {
|
||||
const normalized = instructions.replace(/\s+/g, ' ').trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
const maxLength = 500;
|
||||
const clipped = normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}…` : normalized;
|
||||
return [`Instructions: ${clipped}`];
|
||||
}
|
||||
|
||||
export function printToolDetail(
|
||||
definition: ReturnType<Awaited<ReturnType<(typeof import('../runtime.js'))['createRuntime']>>['getDefinition']>,
|
||||
metadata: ToolMetadata,
|
||||
|
||||
@ -148,6 +148,37 @@ describe('CLI list formatting', () => {
|
||||
metadataSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('surfaces initialize instructions in single server text and JSON output', async () => {
|
||||
const { handleList } = await cliModulePromise;
|
||||
const definition: ServerDefinition = {
|
||||
name: 'immich',
|
||||
description: 'Immich MCP',
|
||||
command: { kind: 'http', url: new URL('https://example.com/mcp') },
|
||||
};
|
||||
const runtime = {
|
||||
getDefinitions: () => [definition],
|
||||
getDefinition: () => definition,
|
||||
listTools: vi.fn().mockResolvedValue([{ name: 'search_assets' }]),
|
||||
connect: vi.fn().mockResolvedValue({
|
||||
client: {
|
||||
getInstructions: () => 'Use asset IDs from search results when calling mutation tools.',
|
||||
},
|
||||
}),
|
||||
} as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await handleList(runtime, ['immich']);
|
||||
let lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
|
||||
expect(lines.some((line) => line.includes('Instructions: Use asset IDs from search results'))).toBe(true);
|
||||
|
||||
logSpy.mockClear();
|
||||
await handleList(runtime, ['--json', 'immich']);
|
||||
const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
|
||||
expect(payload.instructions).toBe('Use asset IDs from search results when calling mutation tools.');
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('prints only the selected tool when listing server.tool with schemas', async () => {
|
||||
const { handleList } = await cliModulePromise;
|
||||
const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user