blob: a8e495f7346ad9a261fb4b46a9891c4193c3e481 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { MCP_CONFIG_VERSION } from '@maka/core/mcp';
import { McpClientManager } from '@maka/mcp';
import { createMcpFormFixture } from '@maka/mcp/test-only/form-server';
import { withClientCapabilityFormHost } from '@maka/runtime-host/test-only/client-capability-host';
import { createMcpCapabilityProvider } from '../mcp-capability-provider.js';
const values = { name: 'Ada', email: 'ada@example.com', confirm: true };
for (const action of ['accept', 'decline', 'cancel'] as const) {
test(`TUI modern MCP ${action} travels through the canonical Host form and back to the server`, {
timeout: 15_000,
}, async () => {
await withFixture(async (manager, server, provider) => {
await withClientCapabilityFormHost(provider, async (host) => {
const result = host.start();
const pending = await host.pending();
assert.equal(pending.request.kind, 'form');
assert.equal(server.calls.length, 1);
// The real broker has paused its execution timer while the Host owns the form.
assert.equal(host.timers.size, 0);
await new Promise<void>((resolve) => setTimeout(resolve, action === 'accept' ? 1_100 : 30));
assert.equal(host.timers.size, 0);
assert.equal((await host.store.listSessionPending('session_1')).length, 1);
if (action === 'accept') {
const rejected = await host.answer(pending.requestId, {
action,
values: { ...values, confirm: 'yes' },
});
assert.equal(rejected.ok, false);
assert.equal(server.calls.length, 1);
}
const answered = await host.answer(
pending.requestId,
action === 'accept' ? { action, values } : { action },
);
assert.equal(answered.ok, true, JSON.stringify(answered));
const settled = await result;
assert.deepEqual(settled.result, { content: [{ type: 'text', text: 'complete' }] });
assert.equal(server.calls.length, 2);
assert.notEqual(server.calls[0]?.id, server.calls[1]?.id);
assert.deepEqual(server.calls[1]?.params.arguments, server.calls[0]?.params.arguments);
assert.equal(server.calls[1]?.params.requestState, 'opaque-state');
assert.deepEqual(server.calls[1]?.params.inputResponses, {
form: action === 'accept' ? { action, content: values } : { action },
});
assert.equal((await host.store.listSessionPending('session_1')).length, 0);
assert.equal(host.events.filter((event) => event.type === 'form_answer_ack').length, 1);
assert.equal(JSON.stringify(host.events).includes('opaque-state'), false);
assert.ok(manager.status('fixture'));
});
});
});
}
test('TUI handles a second MCP question as a new canonical Host form', {
timeout: 15_000,
}, async () => {
await withFixture(async (_manager, server, provider) => {
const originalRespond = server.respond;
let answeredRounds = 0;
server.respond = (params) => {
if (params.inputResponses && ++answeredRounds === 1)
return originalRespond({ name: params.name });
return originalRespond(params);
};
await withClientCapabilityFormHost(provider, async (host) => {
const result = host.start();
const first = await host.pending();
assert.equal((await host.answer(first.requestId, { action: 'decline' })).ok, true);
const second = await host.pending();
assert.notEqual(second.requestId, first.requestId);
assert.equal(server.calls.length, 2);
assert.equal((await host.answer(second.requestId, { action: 'accept', values })).ok, true);
assert.deepEqual((await result).result, { content: [{ type: 'text', text: 'complete' }] });
assert.equal(server.calls.length, 3);
assert.deepEqual(server.calls[1]?.params.inputResponses, { form: { action: 'decline' } });
assert.deepEqual(server.calls[2]?.params.inputResponses, {
form: { action: 'accept', content: values },
});
assert.equal((await host.store.listSessionPending('session_1')).length, 0);
});
});
});
for (const exit of ['stop', 'disconnect', 'provider loss'] as const) {
test(`TUI ${exit} closes a pending modern MCP Host form without a user answer or retry`, {
timeout: 15_000,
}, async () => {
await withFixture(async (manager, server, provider) => {
await withClientCapabilityFormHost(provider, async (host) => {
const result = host.start();
const pending = await host.pending();
if (exit === 'stop') await host.stop();
else if (exit === 'provider loss') await host.disconnectProvider();
else await manager.disconnect('fixture');
await result;
const stored = await host.store.readInteraction(pending.requestId);
assert.equal(stored?.outcome?.outcome.kind, 'closure');
assert.equal((await host.store.listSessionPending('session_1')).length, 0);
assert.equal(server.calls.length, 1);
assert.equal(host.events.filter((event) => event.type === 'form_answer_ack').length, 0);
assert.equal(
(await host.answer(pending.requestId, { action: 'accept', values })).ok,
false,
);
assert.equal(host.timers.size, 0);
});
});
});
}
async function withFixture(
run: (
manager: McpClientManager,
server: Awaited<ReturnType<typeof createMcpFormFixture>>,
provider: ReturnType<typeof createProvider>,
) => Promise<void>,
) {
const server = await createMcpFormFixture();
const manager = new McpClientManager({ timeouts: { callToolMs: 1_000 } });
try {
await manager.sync({
version: MCP_CONFIG_VERSION,
mcpServers: {
fixture: { url: server.url, transport: 'streamable-http', protocol: '2026-07-28' },
},
});
const provider = createProvider(manager);
await run(manager, server, provider);
} finally {
await manager.close();
await server.close();
}
}
function createProvider(manager: McpClientManager) {
const provider = createMcpCapabilityProvider(manager);
assert.ok(provider);
return provider;
}