mesh: POST /api/respond + MCP respond tool — reply to a message by packet id
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
nodeInfoFrame,
|
||||
startBridge,
|
||||
telemetryPacketFrame,
|
||||
textPacketFrame,
|
||||
waitFor,
|
||||
} from './fake-bridge.js';
|
||||
import { createMeshMcpServer } from './mcp.controller.js';
|
||||
@@ -67,7 +68,7 @@ async function connectMcp(svc: MeshService): Promise<{
|
||||
}
|
||||
|
||||
describe('MCP server tools', () => {
|
||||
it('lists all nine tools with descriptions and input schemas', async () => {
|
||||
it('lists all ten tools with descriptions and input schemas', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
const mcp = await connectMcp(env.svc);
|
||||
@@ -82,6 +83,7 @@ describe('MCP server tools', () => {
|
||||
'list_channels',
|
||||
'list_nodes',
|
||||
'mesh_status',
|
||||
'respond',
|
||||
'send_message',
|
||||
]);
|
||||
for (const t of tools) expect(t.description?.length).toBeGreaterThan(0);
|
||||
@@ -100,6 +102,13 @@ describe('MCP server tools', () => {
|
||||
'text',
|
||||
'from',
|
||||
]);
|
||||
|
||||
const respond = tools.find((t) => t.name === 'respond')!;
|
||||
expect(respond.inputSchema.required).toEqual(['messageId', 'content']);
|
||||
expect(Object.keys(respond.inputSchema.properties ?? {})).toEqual([
|
||||
'messageId',
|
||||
'content',
|
||||
]);
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
@@ -196,6 +205,57 @@ describe('MCP server tools', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('respond routes the reply automatically and surfaces failures as tool errors', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
// rx DM from KAMIL on channel 3 (packet id 1)
|
||||
env.sockets[0].write(textPacketFrame(KAMIL_NUM, 42, 'ping', 3));
|
||||
await waitFor(() => env.svc.messages().length === 1);
|
||||
|
||||
const mcp = await connectMcp(env.svc);
|
||||
try {
|
||||
const r1 = await mcp.client.callTool({
|
||||
name: 'respond',
|
||||
arguments: { messageId: 1, content: 'pong' },
|
||||
});
|
||||
expect(r1.isError).not.toBe(true);
|
||||
const reply = JSON.parse(toolText(r1)) as { id: number };
|
||||
expect(reply.id).toBeGreaterThan(0);
|
||||
// DM back to the original sender on the original channel
|
||||
expect(env.svc.messages()[0]).toMatchObject({
|
||||
dir: 'tx',
|
||||
to: KAMIL_NUM,
|
||||
text: 'pong',
|
||||
channel: 3,
|
||||
});
|
||||
|
||||
const r2 = await mcp.client.callTool({
|
||||
name: 'respond',
|
||||
arguments: { messageId: 999, content: 'x' },
|
||||
});
|
||||
expect(r2.isError).toBe(true);
|
||||
expect(toolText(r2)).toContain('no message with id 999');
|
||||
|
||||
const r3 = await mcp.client.callTool({
|
||||
name: 'respond',
|
||||
arguments: { messageId: 0, content: 'x' },
|
||||
});
|
||||
expect(r3.isError).toBe(true); // out-of-range rejected before the service sees it
|
||||
|
||||
const r4 = await mcp.client.callTool({
|
||||
name: 'respond',
|
||||
arguments: { messageId: 1, content: '' },
|
||||
});
|
||||
expect(r4.isError).toBe(true);
|
||||
expect(toolText(r4)).toContain('content must be a non-empty string');
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
} finally {
|
||||
env.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('inject_message records an rx entry without sending over RF', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user