56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import type { CallToolResult } from "@modelcontextprotocol/server";
|
|
|
|
type McpResponseMeta = {
|
|
url?: string;
|
|
organizationId?: string;
|
|
projectId?: string;
|
|
runId?: string;
|
|
creditsCharged?: number;
|
|
creditsRemaining?: number;
|
|
};
|
|
|
|
// The generic overload preserves each tool's concrete structuredContent shape
|
|
// so tests (and callers) can access fields without casting. The type parameter
|
|
// appears in exactly one position — a required `structuredContent: T` — which
|
|
// is what makes inference work; a `T` shared between an optional field and an
|
|
// intersection member collapses to `{}`.
|
|
export function mcpResponse<T extends Record<string, unknown>>(opts: {
|
|
text: string;
|
|
meta?: McpResponseMeta;
|
|
structuredContent: T;
|
|
}): CallToolResult & {
|
|
structuredContent: T & { meta?: Record<string, unknown> };
|
|
};
|
|
export function mcpResponse(opts: {
|
|
text: string;
|
|
meta?: McpResponseMeta;
|
|
}): CallToolResult;
|
|
export function mcpResponse(opts: {
|
|
text: string;
|
|
meta?: McpResponseMeta;
|
|
structuredContent?: Record<string, unknown>;
|
|
}): CallToolResult {
|
|
const result: CallToolResult = {
|
|
content: [{ type: "text", text: opts.text }],
|
|
};
|
|
let meta: Record<string, unknown> | undefined;
|
|
if (opts.meta) {
|
|
meta = {};
|
|
for (const [key, value] of Object.entries(opts.meta)) {
|
|
if (value !== undefined) meta[key] = value;
|
|
}
|
|
}
|
|
const hasMeta = meta != null && Object.keys(meta).length > 0;
|
|
if (opts.structuredContent) {
|
|
result.structuredContent = hasMeta
|
|
? { ...opts.structuredContent, meta }
|
|
: opts.structuredContent;
|
|
} else if (hasMeta) {
|
|
result.structuredContent = { meta };
|
|
}
|
|
if (hasMeta) {
|
|
result._meta = meta;
|
|
}
|
|
return result;
|
|
}
|