138 lines
4.3 KiB
TypeScript
138 lines
4.3 KiB
TypeScript
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
import type { ToolListItem } from '@aaif/goose-sdk';
|
|
import type { GooseApp } from '../types/apps';
|
|
import { getAcpClient } from './acpConnection';
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
export type McpAppTool = ToolListItem;
|
|
export type McpAppResourceResponse = {
|
|
uri: string;
|
|
mimeType: string | null;
|
|
text: string;
|
|
_meta?: Record<string, unknown>;
|
|
};
|
|
type ToolCallResponseLike = {
|
|
content?: Array<unknown>;
|
|
structuredContent?: unknown;
|
|
isError?: boolean;
|
|
_meta?: unknown;
|
|
};
|
|
|
|
function isRecord(value: unknown): value is JsonRecord {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function stringField(record: JsonRecord, key: string): string | undefined {
|
|
const value = record[key];
|
|
return typeof value === 'string' ? value : undefined;
|
|
}
|
|
|
|
function metaField(record: JsonRecord): McpAppResourceResponse['_meta'] {
|
|
const meta = record._meta ?? record.meta;
|
|
return isRecord(meta) ? meta : undefined;
|
|
}
|
|
|
|
function decodeBase64Text(blob: string): string {
|
|
let bytes: Uint8Array;
|
|
if (typeof globalThis.atob === 'function') {
|
|
const binary = globalThis.atob(blob);
|
|
bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
} else {
|
|
bytes = Uint8Array.from(Buffer.from(blob, 'base64'));
|
|
}
|
|
return new TextDecoder().decode(bytes);
|
|
}
|
|
|
|
function flattenReadResourceResult(result: unknown, fallbackUri: string): McpAppResourceResponse {
|
|
const contents = isRecord(result) && Array.isArray(result.contents) ? result.contents : [];
|
|
const first = contents.find(isRecord);
|
|
if (!first) {
|
|
throw new Error(`Resource '${fallbackUri}' returned no contents`);
|
|
}
|
|
|
|
const uri = stringField(first, 'uri') ?? fallbackUri;
|
|
const mimeType = stringField(first, 'mimeType') ?? stringField(first, 'mime_type') ?? null;
|
|
const text = stringField(first, 'text') ?? decodeBase64Text(stringField(first, 'blob') ?? '');
|
|
|
|
return {
|
|
uri,
|
|
mimeType,
|
|
text,
|
|
_meta: metaField(first),
|
|
};
|
|
}
|
|
|
|
function acpApp(value: unknown): GooseApp | null {
|
|
if (!isRecord(value)) return null;
|
|
return value as GooseApp;
|
|
}
|
|
|
|
export async function listMcpApps(sessionId?: string): Promise<GooseApp[]> {
|
|
const client = await getAcpClient();
|
|
const response = await client.goose.appsList_unstable(sessionId ? { sessionId } : {});
|
|
return (response.apps ?? []).map(acpApp).filter((app): app is GooseApp => !!app);
|
|
}
|
|
|
|
export async function exportMcpApp(name: string): Promise<string> {
|
|
const client = await getAcpClient();
|
|
const response = await client.goose.appsExport_unstable({ name });
|
|
return response.html;
|
|
}
|
|
|
|
export async function importMcpApp(html: string): Promise<void> {
|
|
const client = await getAcpClient();
|
|
await client.goose.appsImport_unstable({ html });
|
|
}
|
|
|
|
export async function listMcpAppTools(
|
|
sessionId: string,
|
|
extensionName?: string
|
|
): Promise<McpAppTool[]> {
|
|
const client = await getAcpClient();
|
|
const response = await client.goose.toolsList_unstable({ sessionId });
|
|
const tools = response.tools;
|
|
if (!extensionName) return tools;
|
|
|
|
const prefix = `${extensionName}__`;
|
|
return tools.filter((tool) => tool.name.startsWith(prefix));
|
|
}
|
|
|
|
export async function readMcpAppResource(
|
|
sessionId: string,
|
|
extensionName: string,
|
|
uri: string
|
|
): Promise<McpAppResourceResponse> {
|
|
const client = await getAcpClient();
|
|
const response = await client.goose.resourcesRead_unstable({
|
|
sessionId,
|
|
uri,
|
|
extensionName,
|
|
});
|
|
return flattenReadResourceResult(response.result, uri);
|
|
}
|
|
|
|
export async function callMcpAppTool(
|
|
sessionId: string,
|
|
extensionName: string,
|
|
name: string,
|
|
args: Record<string, unknown> | undefined
|
|
): Promise<CallToolResult> {
|
|
const fullToolName = `${extensionName}__${name}`;
|
|
const client = await getAcpClient();
|
|
const response = await client.goose.toolsCall_unstable({
|
|
sessionId,
|
|
name: fullToolName,
|
|
arguments: args || {},
|
|
});
|
|
return callToolResponseToMcpResult(response);
|
|
}
|
|
|
|
function callToolResponseToMcpResult(response: ToolCallResponseLike | undefined): CallToolResult {
|
|
return {
|
|
content: (response?.content || []) as unknown as CallToolResult['content'],
|
|
isError: response?.isError || false,
|
|
structuredContent: response?.structuredContent as { [key: string]: unknown } | undefined,
|
|
_meta: response?._meta as { [key: string]: unknown } | undefined,
|
|
};
|
|
}
|