Inbox injection endpoint: POST /api/messages + MCP inject_message (#12) (#18)
CI / build + unit (push) Successful in 1m14s
CI / build + unit (push) Successful in 1m14s
This commit was merged in pull request #18.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { fromBinary } from '@bufbuild/protobuf';
|
||||
import * as P from '@meshtastic/protobufs';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
||||
import type * as net from 'node:net';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { BROADCAST_ADDR } from './codec.js';
|
||||
import { BROADCAST_ADDR, extractFrames } from './codec.js';
|
||||
import {
|
||||
KAMIL_NUM,
|
||||
kamilNodeInfo,
|
||||
@@ -65,7 +67,7 @@ async function connectMcp(svc: MeshService): Promise<{
|
||||
}
|
||||
|
||||
describe('MCP server tools', () => {
|
||||
it('lists all eight tools with descriptions and input schemas', async () => {
|
||||
it('lists all nine tools with descriptions and input schemas', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
const mcp = await connectMcp(env.svc);
|
||||
@@ -76,6 +78,7 @@ describe('MCP server tools', () => {
|
||||
'get_messages',
|
||||
'get_node',
|
||||
'get_telemetry',
|
||||
'inject_message',
|
||||
'list_channels',
|
||||
'list_nodes',
|
||||
'mesh_status',
|
||||
@@ -89,6 +92,13 @@ describe('MCP server tools', () => {
|
||||
'text',
|
||||
'to',
|
||||
]);
|
||||
|
||||
const inject = tools.find((t) => t.name === 'inject_message')!;
|
||||
expect(inject.inputSchema.required).toEqual(['text']);
|
||||
expect(Object.keys(inject.inputSchema.properties ?? {})).toEqual([
|
||||
'text',
|
||||
'from',
|
||||
]);
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
@@ -185,6 +195,68 @@ describe('MCP server tools', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('inject_message records an rx entry without sending over RF', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
const mcp = await connectMcp(env.svc);
|
||||
try {
|
||||
// capture outbound bytes: the only frame ever written on connect is want_config,
|
||||
// so any 'packet' variant after this point would be an RF transmission
|
||||
const out: Uint8Array[] = [];
|
||||
env.sockets[0].on('data', (c) => out.push(c));
|
||||
|
||||
const r1 = await mcp.client.callTool({
|
||||
name: 'inject_message',
|
||||
arguments: { text: 'trigger' },
|
||||
});
|
||||
expect(r1.isError).not.toBe(true);
|
||||
const inj = JSON.parse(toolText(r1)) as { id: number };
|
||||
expect(inj.id).toBeGreaterThan(0);
|
||||
expect(env.svc.messages()[0]).toMatchObject({
|
||||
dir: 'rx',
|
||||
text: 'trigger',
|
||||
channel: 0,
|
||||
});
|
||||
|
||||
const r2 = await mcp.client.callTool({
|
||||
name: 'inject_message',
|
||||
arguments: { text: 'from kamil', from: KAMIL_NUM },
|
||||
});
|
||||
expect(r2.isError).not.toBe(true);
|
||||
expect(env.svc.messages()[0]).toMatchObject({
|
||||
dir: 'rx',
|
||||
from: KAMIL_NUM,
|
||||
text: 'from kamil',
|
||||
});
|
||||
|
||||
const r3 = await mcp.client.callTool({
|
||||
name: 'inject_message',
|
||||
arguments: { text: 'x', from: 0 },
|
||||
});
|
||||
expect(r3.isError).toBe(true);
|
||||
expect(toolText(r3)).toContain('invalid node number');
|
||||
|
||||
// nothing went over the wire — synthetic inbox injection only.
|
||||
// The want_config frame (written on connect) may or may not have been captured;
|
||||
// what matters is that no ToRadio 'packet' variant ever made it to the bridge.
|
||||
const merged = new Uint8Array(out.reduce((n, c) => n + c.length, 0));
|
||||
let off = 0;
|
||||
for (const c of out) {
|
||||
merged.set(c, off);
|
||||
off += c.length;
|
||||
}
|
||||
for (const p of extractFrames(merged).packets) {
|
||||
const tr = fromBinary(P.Mesh.ToRadioSchema, p);
|
||||
expect(tr.payloadVariant?.case).not.toBe('packet');
|
||||
}
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
} finally {
|
||||
env.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('get_node resolves by name and errors when missing', async () => {
|
||||
const env = await connectedService();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user