mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
1722 lines
67 KiB
C
1722 lines
67 KiB
C
/*
|
|
* Tardygrada — MCP Server Implementation
|
|
* JSON-RPC 2.0 over stdin/stdout.
|
|
*
|
|
* A Tardygrada program compiles to this.
|
|
* The world connects. Asks questions. Gets verified responses.
|
|
*/
|
|
|
|
#include "server.h"
|
|
#include "vm/util.h"
|
|
#include "../verify/pipeline.h"
|
|
#include "../verify/decompose.h"
|
|
#include "../verify/preprocess.h"
|
|
#include "../vm/semantic.h"
|
|
#include "../ontology/inference.h"
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <fcntl.h>
|
|
#include <sys/wait.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
static uint64_t mcp_now_ns(void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
|
}
|
|
|
|
/* Direct write — no stdio */
|
|
static void mcp_write(const char *data, int len)
|
|
{
|
|
/* MCP stdio transport: Content-Length header + \r\n\r\n + body */
|
|
char header[64];
|
|
int hlen = 0;
|
|
const char *prefix = "Content-Length: ";
|
|
int plen = (int)strlen(prefix);
|
|
memcpy(header, prefix, plen);
|
|
hlen = plen;
|
|
|
|
/* itoa for content length */
|
|
char num[16];
|
|
int nlen = 0;
|
|
int tmp = len;
|
|
if (tmp == 0) {
|
|
num[nlen++] = '0';
|
|
} else {
|
|
while (tmp > 0) {
|
|
num[nlen++] = '0' + (tmp % 10);
|
|
tmp /= 10;
|
|
}
|
|
}
|
|
for (int i = nlen - 1; i >= 0; i--)
|
|
header[hlen++] = num[i];
|
|
header[hlen++] = '\r';
|
|
header[hlen++] = '\n';
|
|
header[hlen++] = '\r';
|
|
header[hlen++] = '\n';
|
|
|
|
tardy_write(STDOUT_FILENO, header, hlen);
|
|
tardy_write(STDOUT_FILENO, data, len);
|
|
}
|
|
|
|
/* Build a JSON-RPC response */
|
|
static int build_response(char *buf, int buf_size, const char *id,
|
|
const char *result)
|
|
{
|
|
int written = 0;
|
|
const char *pre = "{\"jsonrpc\":\"2.0\",\"id\":";
|
|
int prelen = (int)strlen(pre);
|
|
if (written + prelen >= buf_size) return -1;
|
|
memcpy(buf + written, pre, prelen);
|
|
written += prelen;
|
|
|
|
int idlen = (int)strlen(id);
|
|
if (written + idlen >= buf_size) return -1;
|
|
memcpy(buf + written, id, idlen);
|
|
written += idlen;
|
|
|
|
const char *mid = ",\"result\":";
|
|
int midlen = (int)strlen(mid);
|
|
if (written + midlen >= buf_size) return -1;
|
|
memcpy(buf + written, mid, midlen);
|
|
written += midlen;
|
|
|
|
int rlen = (int)strlen(result);
|
|
if (written + rlen >= buf_size) return -1;
|
|
memcpy(buf + written, result, rlen);
|
|
written += rlen;
|
|
|
|
if (written + 1 >= buf_size) return -1;
|
|
buf[written++] = '}';
|
|
buf[written] = '\0';
|
|
return written;
|
|
}
|
|
|
|
/* Build an error response */
|
|
static int build_error(char *buf, int buf_size, const char *id,
|
|
int code, const char *message)
|
|
{
|
|
int written = 0;
|
|
const char *pre = "{\"jsonrpc\":\"2.0\",\"id\":";
|
|
int prelen = (int)strlen(pre);
|
|
if (written + prelen >= buf_size) return -1;
|
|
memcpy(buf + written, pre, prelen);
|
|
written += prelen;
|
|
|
|
int idlen = (int)strlen(id);
|
|
if (written + idlen >= buf_size) return -1;
|
|
memcpy(buf + written, id, idlen);
|
|
written += idlen;
|
|
|
|
const char *mid = ",\"error\":{\"code\":";
|
|
int midlen = (int)strlen(mid);
|
|
if (written + midlen >= buf_size) return -1;
|
|
memcpy(buf + written, mid, midlen);
|
|
written += midlen;
|
|
|
|
/* code as string */
|
|
char codestr[16];
|
|
int clen = 0;
|
|
int neg = 0;
|
|
int c = code;
|
|
if (c < 0) { neg = 1; c = -c; }
|
|
if (c == 0) { codestr[clen++] = '0'; }
|
|
while (c > 0) { codestr[clen++] = '0' + (c % 10); c /= 10; }
|
|
if (neg) codestr[clen++] = '-';
|
|
for (int i = 0; i < clen / 2; i++) {
|
|
char t = codestr[i];
|
|
codestr[i] = codestr[clen - 1 - i];
|
|
codestr[clen - 1 - i] = t;
|
|
}
|
|
if (written + clen >= buf_size) return -1;
|
|
memcpy(buf + written, codestr, clen);
|
|
written += clen;
|
|
|
|
const char *msgpre = ",\"message\":\"";
|
|
int mplen = (int)strlen(msgpre);
|
|
if (written + mplen >= buf_size) return -1;
|
|
memcpy(buf + written, msgpre, mplen);
|
|
written += mplen;
|
|
|
|
int mlen = (int)strlen(message);
|
|
if (written + mlen >= buf_size) return -1;
|
|
memcpy(buf + written, message, mlen);
|
|
written += mlen;
|
|
|
|
const char *suf = "\"}}";
|
|
int suflen = (int)strlen(suf);
|
|
if (written + suflen >= buf_size) return -1;
|
|
memcpy(buf + written, suf, suflen);
|
|
written += suflen;
|
|
|
|
buf[written] = '\0';
|
|
return written;
|
|
}
|
|
|
|
/* ============================================
|
|
* Provenance Helpers
|
|
* ============================================ */
|
|
|
|
static const char *trust_str(tardy_trust_t t)
|
|
{
|
|
switch (t) {
|
|
case TARDY_TRUST_MUTABLE: return "mutable";
|
|
case TARDY_TRUST_DEFAULT: return "immutable";
|
|
case TARDY_TRUST_VERIFIED: return "verified";
|
|
case TARDY_TRUST_HARDENED: return "hardened";
|
|
case TARDY_TRUST_SOVEREIGN: return "sovereign";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
static const char *state_str(tardy_state_t s)
|
|
{
|
|
switch (s) {
|
|
case TARDY_STATE_LIVE: return "live";
|
|
case TARDY_STATE_STATIC: return "static";
|
|
case TARDY_STATE_TEMP: return "temp";
|
|
case TARDY_STATE_DEAD: return "dead";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
static const char *type_str(tardy_type_t t)
|
|
{
|
|
switch (t) {
|
|
case TARDY_TYPE_INT: return "int";
|
|
case TARDY_TYPE_FLOAT: return "float";
|
|
case TARDY_TYPE_STR: return "str";
|
|
case TARDY_TYPE_BOOL: return "bool";
|
|
case TARDY_TYPE_FACT: return "fact";
|
|
case TARDY_TYPE_AGENT: return "agent";
|
|
case TARDY_TYPE_UNIT: return "unit";
|
|
case TARDY_TYPE_ERROR: return "error";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
/* Append _tardy provenance JSON to a result buffer.
|
|
* buf: buffer with existing JSON (without closing '}')
|
|
* pos: current write position in buf
|
|
* buf_size: total buffer size
|
|
* r: the read result with provenance data
|
|
* Returns new position, or -1 on overflow.
|
|
*/
|
|
static int append_provenance(char *buf, int pos, int buf_size,
|
|
const tardy_read_result_t *r,
|
|
const char *ontology_status)
|
|
{
|
|
/* Build the birth_hash hex string (first 16 bytes = 32 hex chars) */
|
|
char hash_hex[65];
|
|
int hbytes = 16;
|
|
for (int i = 0; i < hbytes; i++) {
|
|
uint8_t b = r->provenance.birth_hash.bytes[i];
|
|
hash_hex[i * 2] = "0123456789abcdef"[b >> 4];
|
|
hash_hex[i * 2 + 1] = "0123456789abcdef"[b & 0x0f];
|
|
}
|
|
hash_hex[hbytes * 2] = '\0';
|
|
|
|
const char *reason = r->provenance.reason ? r->provenance.reason : "unknown";
|
|
const char *ont = ontology_status ? ontology_status : "unknown";
|
|
|
|
/* Format the _tardy object */
|
|
char tardy_json[512];
|
|
int tlen = snprintf(tardy_json, sizeof(tardy_json),
|
|
",\"_tardy\":{"
|
|
"\"trust\":\"%s\","
|
|
"\"state\":\"%s\","
|
|
"\"type\":\"%s\","
|
|
"\"created_at\":%llu,"
|
|
"\"reason\":\"%s\","
|
|
"\"birth_hash\":\"%s\","
|
|
"\"ontology\":\"%s\""
|
|
"}",
|
|
trust_str(r->trust),
|
|
state_str(r->state),
|
|
type_str(r->type_tag),
|
|
(unsigned long long)r->provenance.created_at,
|
|
reason,
|
|
hash_hex,
|
|
ont);
|
|
|
|
if (tlen < 0 || pos + tlen >= buf_size)
|
|
return -1;
|
|
memcpy(buf + pos, tardy_json, tlen);
|
|
pos += tlen;
|
|
return pos;
|
|
}
|
|
|
|
/* ============================================
|
|
* MCP Method Handlers
|
|
* ============================================ */
|
|
|
|
/* initialize — MCP handshake */
|
|
static int handle_initialize(tardy_mcp_server_t *srv, const char *id)
|
|
{
|
|
const char *result =
|
|
"{\"protocolVersion\":\"2024-11-05\","
|
|
"\"capabilities\":{\"tools\":{}},"
|
|
"\"serverInfo\":{\"name\":\"tardygrada\",\"version\":\"0.1.0\"}}";
|
|
int len = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE, id, result);
|
|
if (len > 0)
|
|
mcp_write(srv->write_buf, len);
|
|
return 0;
|
|
}
|
|
|
|
/* Emit one tool entry into the buffer. Returns bytes written. */
|
|
static int emit_tool(char *buf, int buf_size, int pos, const char *name,
|
|
tardy_agent_t *agent, int *first)
|
|
{
|
|
int tlen = pos;
|
|
if (!*first && tlen < buf_size) buf[tlen++] = ',';
|
|
*first = 0;
|
|
|
|
const char *pre = "{\"name\":\"";
|
|
int prelen = (int)strlen(pre);
|
|
if (tlen + prelen >= buf_size) return tlen;
|
|
memcpy(buf + tlen, pre, prelen);
|
|
tlen += prelen;
|
|
|
|
int nlen = (int)strlen(name);
|
|
if (tlen + nlen >= buf_size) return tlen;
|
|
memcpy(buf + tlen, name, nlen);
|
|
tlen += nlen;
|
|
|
|
const char *mid_desc;
|
|
if (agent && agent->type_tag == TARDY_TYPE_ERROR)
|
|
mid_desc = "\",\"description\":\"error agent\",";
|
|
else if (agent && agent->trust >= TARDY_TRUST_SOVEREIGN)
|
|
mid_desc = "\",\"description\":\"sovereign agent\",";
|
|
else if (agent && agent->trust >= TARDY_TRUST_VERIFIED)
|
|
mid_desc = "\",\"description\":\"verified agent\",";
|
|
else if (agent && agent->trust >= TARDY_TRUST_DEFAULT)
|
|
mid_desc = "\",\"description\":\"immutable agent\",";
|
|
else
|
|
mid_desc = "\",\"description\":\"mutable agent\",";
|
|
|
|
int mdlen = (int)strlen(mid_desc);
|
|
if (tlen + mdlen >= buf_size) return tlen;
|
|
memcpy(buf + tlen, mid_desc, mdlen);
|
|
tlen += mdlen;
|
|
|
|
const char *schema = "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}";
|
|
int slen = (int)strlen(schema);
|
|
if (tlen + slen >= buf_size) return tlen;
|
|
memcpy(buf + tlen, schema, slen);
|
|
tlen += slen;
|
|
return tlen;
|
|
}
|
|
|
|
/* tools/list — expose ALL agents in the tree as tools */
|
|
static int handle_tools_list(tardy_mcp_server_t *srv, const char *id)
|
|
{
|
|
char tools[4096];
|
|
int tlen = 0;
|
|
tools[tlen++] = '{';
|
|
const char *tp = "\"tools\":[";
|
|
int tplen = (int)strlen(tp);
|
|
memcpy(tools + tlen, tp, tplen);
|
|
tlen += tplen;
|
|
|
|
int first = 1;
|
|
|
|
/* Walk all agents (skip root at index 0), expose value agents as tools */
|
|
for (int i = 1; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *a = &srv->vm->agents[i];
|
|
if (a->state == TARDY_STATE_DEAD)
|
|
continue;
|
|
if (a->type_tag == TARDY_TYPE_AGENT)
|
|
continue; /* skip container agents, expose values only */
|
|
|
|
/* Find this agent's name by scanning all parents' contexts */
|
|
const char *name = NULL;
|
|
for (int p = 0; p < srv->vm->agent_count; p++) {
|
|
tardy_agent_t *parent = &srv->vm->agents[p];
|
|
for (int c = 0; c < parent->context.child_count; c++) {
|
|
if (parent->context.children[c].agent_id.hi == a->id.hi &&
|
|
parent->context.children[c].agent_id.lo == a->id.lo) {
|
|
name = parent->context.children[c].name;
|
|
break;
|
|
}
|
|
}
|
|
if (name) break;
|
|
}
|
|
|
|
if (name)
|
|
tlen = emit_tool(tools, sizeof(tools), tlen, name, a, &first);
|
|
}
|
|
|
|
/* Built-in tools: submit_claim and verify_claim */
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *sc =
|
|
"{\"name\":\"submit_claim\","
|
|
"\"description\":\"Submit a claim for a pending agent\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent\":{\"type\":\"string\"},"
|
|
"\"claim\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent\",\"claim\"]}}";
|
|
int sclen = (int)strlen(sc);
|
|
if (tlen + sclen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, sc, sclen);
|
|
tlen += sclen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *vc =
|
|
"{\"name\":\"verify_claim\","
|
|
"\"description\":\"Verify a pending agent's claim through the pipeline\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent\"]}}";
|
|
int vclen = (int)strlen(vc);
|
|
if (tlen + vclen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, vc, vclen);
|
|
tlen += vclen;
|
|
}
|
|
}
|
|
/* Built-in tools: send_message and read_inbox */
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *sm =
|
|
"{\"name\":\"send_message\","
|
|
"\"description\":\"Send a message from one agent to another\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent_from\":{\"type\":\"string\"},"
|
|
"\"agent_to\":{\"type\":\"string\"},"
|
|
"\"payload\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent_from\",\"agent_to\",\"payload\"]}}";
|
|
int smlen = (int)strlen(sm);
|
|
if (tlen + smlen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, sm, smlen);
|
|
tlen += smlen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *ri =
|
|
"{\"name\":\"read_inbox\","
|
|
"\"description\":\"Read the next message from an agent's inbox\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent\"]}}";
|
|
int rilen = (int)strlen(ri);
|
|
if (tlen + rilen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, ri, rilen);
|
|
tlen += rilen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *ss =
|
|
"{\"name\":\"set_semantics\","
|
|
"\"description\":\"Set per-agent verification thresholds\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent\":{\"type\":\"string\"},"
|
|
"\"key\":{\"type\":\"string\"},"
|
|
"\"value\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent\",\"key\",\"value\"]}}";
|
|
int sslen = (int)strlen(ss);
|
|
if (tlen + sslen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, ss, sslen);
|
|
tlen += sslen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *qa =
|
|
"{\"name\":\"query_agents\","
|
|
"\"description\":\"Search agents by keyword matching\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"query\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"query\"]}}";
|
|
int qalen = (int)strlen(qa);
|
|
if (tlen + qalen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, qa, qalen);
|
|
tlen += qalen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *gc =
|
|
"{\"name\":\"get_conversation\","
|
|
"\"description\":\"Read agent conversation history\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"agent\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"agent\"]}}";
|
|
int gclen = (int)strlen(gc);
|
|
if (tlen + gclen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, gc, gclen);
|
|
tlen += gclen;
|
|
}
|
|
}
|
|
{
|
|
if (!first && tlen < (int)sizeof(tools)) tools[tlen++] = ',';
|
|
first = 0;
|
|
const char *lo =
|
|
"{\"name\":\"load_ontology\","
|
|
"\"description\":\"Load a TTL file into the self-hosted ontology\","
|
|
"\"inputSchema\":{\"type\":\"object\","
|
|
"\"properties\":{\"path\":{\"type\":\"string\"}},"
|
|
"\"required\":[\"path\"]}}";
|
|
int lolen = (int)strlen(lo);
|
|
if (tlen + lolen < (int)sizeof(tools)) {
|
|
memcpy(tools + tlen, lo, lolen);
|
|
tlen += lolen;
|
|
}
|
|
}
|
|
(void)first;
|
|
|
|
tools[tlen++] = ']';
|
|
tools[tlen++] = '}';
|
|
tools[tlen] = '\0';
|
|
|
|
int len = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE, id, tools);
|
|
if (len > 0)
|
|
mcp_write(srv->write_buf, len);
|
|
return 0;
|
|
}
|
|
|
|
/* tools/call — read an agent's value */
|
|
static int handle_tools_call(tardy_mcp_server_t *srv,
|
|
tardy_json_parser_t *parser,
|
|
const char *id)
|
|
{
|
|
/* Get params.name */
|
|
int params_tok = tardy_json_find(parser, 0, "params");
|
|
if (params_tok < 0) {
|
|
int len = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing params");
|
|
if (len > 0) mcp_write(srv->write_buf, len);
|
|
return -1;
|
|
}
|
|
|
|
int name_tok = tardy_json_find(parser, params_tok, "name");
|
|
if (name_tok < 0) {
|
|
int len = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing params.name");
|
|
if (len > 0) mcp_write(srv->write_buf, len);
|
|
return -1;
|
|
}
|
|
|
|
char tool_name[64];
|
|
tardy_json_str(parser, name_tok, tool_name, sizeof(tool_name));
|
|
|
|
/* ---- Built-in: submit_claim ---- */
|
|
if (strcmp(tool_name, "submit_claim") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int agent_tok = tardy_json_find(parser, args_tok, "agent");
|
|
int claim_tok = tardy_json_find(parser, args_tok, "claim");
|
|
if (agent_tok < 0 || claim_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing agent or claim");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char agent_name[64];
|
|
char claim_text[512];
|
|
tardy_json_str(parser, agent_tok, agent_name, sizeof(agent_name));
|
|
tardy_json_str(parser, claim_tok, claim_text, sizeof(claim_text));
|
|
|
|
/* Find and mutate the pending agent */
|
|
int mutated = -1;
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *candidate = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, agent_name);
|
|
if (candidate && candidate->trust == TARDY_TRUST_MUTABLE) {
|
|
mutated = tardy_vm_mutate(srv->vm, srv->vm->agents[i].id,
|
|
agent_name, claim_text,
|
|
strlen(claim_text) + 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mutated == 0) {
|
|
/* Record in agent's conversation */
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *candidate = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, agent_name);
|
|
if (candidate) {
|
|
tardy_vm_converse(srv->vm, candidate->id,
|
|
"user", claim_text);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const char *ok_result =
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"claim submitted\"}]}";
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, ok_result);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
} else {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "agent not found or not mutable");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: verify_claim ---- */
|
|
if (strcmp(tool_name, "verify_claim") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int agent_tok = tardy_json_find(parser, args_tok, "agent");
|
|
if (agent_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing agent");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char agent_name[64];
|
|
tardy_json_str(parser, agent_tok, agent_name, sizeof(agent_name));
|
|
|
|
/* Find the agent */
|
|
tardy_agent_t *target = NULL;
|
|
tardy_uuid_t parent_id = {0, 0};
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *candidate = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, agent_name);
|
|
if (candidate) {
|
|
target = candidate;
|
|
parent_id = srv->vm->agents[i].id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!target) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "agent not found");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
/* Read current value */
|
|
char claim_buf[512];
|
|
memset(claim_buf, 0, sizeof(claim_buf));
|
|
tardy_vm_read(srv->vm, parent_id, agent_name,
|
|
claim_buf, sizeof(claim_buf));
|
|
int claim_len = (int)strlen(claim_buf);
|
|
|
|
/* Run verification pipeline */
|
|
uint64_t verify_start = mcp_now_ns();
|
|
|
|
/* Check if caller provided pre-decomposed triples (LLM decomposer) */
|
|
tardy_decomposition_t decomps[3];
|
|
memset(decomps, 0, sizeof(decomps));
|
|
tardy_triple_t all_triples[TARDY_MAX_TRIPLES];
|
|
int triple_count = 0;
|
|
|
|
/* Step 1: Subagent decomposer (preprocessor + rule-based).
|
|
* Strips markdown, extracts key-value pairs, then decomposes.
|
|
* This is the default -- zero cost, deterministic. */
|
|
triple_count = tardy_preprocess_and_decompose(
|
|
claim_buf, claim_len, all_triples, TARDY_MAX_TRIPLES);
|
|
|
|
/* Also run multi-pass rule decomposer for agreement scoring */
|
|
tardy_decompose_multi(claim_buf, claim_len, decomps, 3);
|
|
|
|
/* Merge any triples from multi-pass that aren't already found */
|
|
for (int d = 0; d < 3; d++) {
|
|
for (int t = 0; t < decomps[d].count &&
|
|
triple_count < TARDY_MAX_TRIPLES; t++) {
|
|
int dup = 0;
|
|
for (int e = 0; e < triple_count; e++) {
|
|
if (strcmp(all_triples[e].subject,
|
|
decomps[d].triples[t].subject) == 0 &&
|
|
strcmp(all_triples[e].predicate,
|
|
decomps[d].triples[t].predicate) == 0 &&
|
|
strcmp(all_triples[e].object,
|
|
decomps[d].triples[t].object) == 0) {
|
|
dup = 1; break;
|
|
}
|
|
}
|
|
if (!dup)
|
|
all_triples[triple_count++] = decomps[d].triples[t];
|
|
}
|
|
}
|
|
|
|
/* Update decomps for pipeline (uses preprocessor triples) */
|
|
for (int t = 0; t < triple_count && t < TARDY_MAX_TRIPLES; t++) {
|
|
if (t < decomps[0].count) continue; /* already there from multi-pass */
|
|
decomps[0].triples[decomps[0].count] = all_triples[t];
|
|
decomps[0].count++;
|
|
decomps[1].triples[decomps[1].count] = all_triples[t];
|
|
decomps[1].count++;
|
|
decomps[2].triples[decomps[2].count] = all_triples[t];
|
|
decomps[2].count++;
|
|
}
|
|
|
|
/* Step 1b: OPTIONAL LLM-assisted decomposition.
|
|
* Only fires when TARDY_LLM_DECOMPOSE=1 is set AND ANTHROPIC_API_KEY exists.
|
|
* Default: subagent preprocessor (zero deps, zero cost, instant).
|
|
* The LLM converts free text to structured triples. */
|
|
const char *api_key = getenv("ANTHROPIC_API_KEY");
|
|
const char *llm_decompose = getenv("TARDY_LLM_DECOMPOSE");
|
|
if (triple_count < 2 && api_key && api_key[0] &&
|
|
llm_decompose && llm_decompose[0] == '1') {
|
|
/* Build LLM prompt asking for structured triple extraction */
|
|
char prompt_body[2048];
|
|
snprintf(prompt_body, sizeof(prompt_body),
|
|
"{\"model\":\"claude-haiku-4-5-20251001\",\"max_tokens\":300,"
|
|
"\"temperature\":0,"
|
|
"\"system\":\"Extract factual claims as JSON triples. "
|
|
"Output ONLY a JSON array like: "
|
|
"[{\\\"s\\\":\\\"subject\\\",\\\"p\\\":\\\"predicate\\\",\\\"o\\\":\\\"object\\\"}]. "
|
|
"Use simple predicates like: created_by, located_in, created_in, "
|
|
"type_of, has, premiered_on, written_by, invented_by. "
|
|
"No explanation.\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"Extract triples: "
|
|
"%.800s\"}]}", claim_buf);
|
|
|
|
/* Fork curl to call Claude Haiku (cheapest, fastest) */
|
|
int pipefd[2];
|
|
if (pipe(pipefd) == 0) {
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
close(pipefd[0]);
|
|
dup2(pipefd[1], STDOUT_FILENO);
|
|
close(pipefd[1]);
|
|
int devnull = open("/dev/null", O_WRONLY);
|
|
if (devnull >= 0) {
|
|
dup2(devnull, STDERR_FILENO);
|
|
close(devnull);
|
|
}
|
|
char auth[300];
|
|
snprintf(auth, sizeof(auth), "x-api-key: %s", api_key);
|
|
execlp("curl", "curl", "-s",
|
|
"-X", "POST",
|
|
"https://api.anthropic.com/v1/messages",
|
|
"-H", "Content-Type: application/json",
|
|
"-H", "anthropic-version: 2023-06-01",
|
|
"-H", auth,
|
|
"-d", prompt_body,
|
|
(char *)NULL);
|
|
_exit(127);
|
|
} else if (pid > 0) {
|
|
close(pipefd[1]);
|
|
char llm_out[4096];
|
|
int llm_len = 0;
|
|
ssize_t n;
|
|
while ((n = read(pipefd[0], llm_out + llm_len,
|
|
sizeof(llm_out) - (size_t)llm_len - 1)) > 0)
|
|
llm_len += (int)n;
|
|
llm_out[llm_len] = '\0';
|
|
close(pipefd[0]);
|
|
int wstatus;
|
|
waitpid(pid, &wstatus, 0);
|
|
|
|
/* Parse LLM response -- find the JSON array in content[0].text */
|
|
tardy_json_parser_t lp;
|
|
if (tardy_json_parse(&lp, llm_out, llm_len) == 0) {
|
|
int ct = tardy_json_find(&lp, 0, "content");
|
|
if (ct >= 0) {
|
|
int ft = ct + 1;
|
|
int tt = tardy_json_find(&lp, ft, "text");
|
|
if (tt >= 0) {
|
|
char text_buf[2048];
|
|
int tlen = tardy_json_str(&lp, tt,
|
|
text_buf, sizeof(text_buf));
|
|
/* Find [ in the text to locate the JSON array */
|
|
char *arr_start = strchr(text_buf, '[');
|
|
if (arr_start && tlen > 0) {
|
|
tardy_json_parser_t tp;
|
|
int arr_len = tlen - (int)(arr_start - text_buf);
|
|
if (tardy_json_parse(&tp, arr_start, arr_len) == 0 &&
|
|
tp.tokens[0].type == TARDY_JSON_ARRAY) {
|
|
int ac = tp.tokens[0].children;
|
|
int ti = 1;
|
|
for (int t = 0; t < ac &&
|
|
triple_count < TARDY_MAX_TRIPLES; t++) {
|
|
int st = tardy_json_find(&tp, ti, "s");
|
|
int pt = tardy_json_find(&tp, ti, "p");
|
|
int ot = tardy_json_find(&tp, ti, "o");
|
|
if (st >= 0 && pt >= 0 && ot >= 0) {
|
|
tardy_json_str(&tp, st,
|
|
all_triples[triple_count].subject,
|
|
TARDY_MAX_TRIPLE_LEN);
|
|
tardy_json_str(&tp, pt,
|
|
all_triples[triple_count].predicate,
|
|
TARDY_MAX_TRIPLE_LEN);
|
|
tardy_json_str(&tp, ot,
|
|
all_triples[triple_count].object,
|
|
TARDY_MAX_TRIPLE_LEN);
|
|
triple_count++;
|
|
}
|
|
ti += 1 + tp.tokens[ti].children;
|
|
}
|
|
/* Update decomps for pipeline */
|
|
for (int t = 0; t < triple_count; t++) {
|
|
decomps[0].triples[t] = all_triples[t];
|
|
decomps[1].triples[t] = all_triples[t];
|
|
decomps[2].triples[t] = all_triples[t];
|
|
}
|
|
decomps[0].count = triple_count;
|
|
decomps[1].count = triple_count;
|
|
decomps[2].count = triple_count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Step 2.5: Try computational verification first */
|
|
float comp_confidence = 0.0f;
|
|
int comp_result = tardy_inference_compute(claim_buf, claim_len,
|
|
&comp_confidence);
|
|
|
|
/* Step 3: Ground triples against ontology (real or fallback) */
|
|
tardy_grounding_t grounding = {0};
|
|
tardy_consistency_t consistency = {0};
|
|
|
|
if (comp_result == 1) {
|
|
/* Computational claim verified — set grounding as fully grounded */
|
|
grounding.count = triple_count;
|
|
for (int i = 0; i < triple_count && i < TARDY_MAX_TRIPLES; i++) {
|
|
grounding.results[i].triple = all_triples[i];
|
|
grounding.results[i].status = TARDY_KNOWLEDGE_GROUNDED;
|
|
grounding.results[i].confidence = comp_confidence;
|
|
grounding.results[i].evidence_count = 1;
|
|
grounding.grounded++;
|
|
}
|
|
consistency.consistent = true;
|
|
} else if (srv->bridge_connected) {
|
|
/* External ontology engine via unix socket */
|
|
tardy_bridge_verify(&srv->bridge, all_triples, triple_count,
|
|
&grounding, &consistency);
|
|
} else if (srv->self_ontology_loaded &&
|
|
srv->self_ontology.triple_count > 0) {
|
|
/* Self-hosted ontology: triples as agents, no external process */
|
|
tardy_self_ontology_verify(&srv->self_ontology,
|
|
all_triples, triple_count,
|
|
&grounding, &consistency);
|
|
} else {
|
|
/* No ontology available: honest UNKNOWN fallback */
|
|
grounding.count = triple_count;
|
|
for (int i = 0; i < triple_count &&
|
|
i < TARDY_MAX_TRIPLES; i++) {
|
|
grounding.results[i].triple = all_triples[i];
|
|
grounding.results[i].status = TARDY_KNOWLEDGE_UNKNOWN;
|
|
grounding.results[i].confidence = 0.0f;
|
|
grounding.results[i].evidence_count = 0;
|
|
grounding.unknown++;
|
|
}
|
|
consistency.consistent = true;
|
|
}
|
|
|
|
/* Step 4: Real work log — record actual operations */
|
|
tardy_work_log_t work_log;
|
|
tardy_worklog_init(&work_log);
|
|
work_log.ontology_queries = (srv->bridge_connected ||
|
|
srv->self_ontology.triple_count > 0) ? triple_count : 0;
|
|
work_log.context_reads = triple_count;
|
|
work_log.agents_spawned = 0;
|
|
|
|
const tardy_semantics_t *sem = tardy_vm_get_semantics(srv->vm, target->id);
|
|
tardy_work_spec_t spec = tardy_compute_work_spec(sem);
|
|
|
|
/* Run pipeline 3 times independently for Byzantine consensus */
|
|
tardy_pipeline_result_t results[3];
|
|
int pass_count = 0;
|
|
float total_confidence = 0.0f;
|
|
|
|
for (int run = 0; run < 3; run++) {
|
|
/* Each run uses slightly different decomposition */
|
|
tardy_decomposition_t run_decomps[3];
|
|
memset(run_decomps, 0, sizeof(run_decomps));
|
|
|
|
/* Rotate which decomposition is primary for each run */
|
|
for (int d = 0; d < 3; d++) {
|
|
int src_idx = (d + run) % 3;
|
|
if (src_idx < 3)
|
|
run_decomps[d] = decomps[src_idx];
|
|
}
|
|
|
|
results[run] = tardy_pipeline_verify(
|
|
claim_buf, claim_len,
|
|
run_decomps, 3, &grounding, &consistency,
|
|
&work_log, &spec, sem);
|
|
|
|
if (results[run].passed) {
|
|
pass_count++;
|
|
total_confidence += results[run].confidence;
|
|
}
|
|
}
|
|
|
|
/* Byzantine majority: 2 of 3 must agree */
|
|
int verified = (pass_count >= 2);
|
|
float avg_confidence = pass_count > 0 ? total_confidence / (float)pass_count : 0.0f;
|
|
tardy_truth_strength_t final_strength = TARDY_TRUTH_HYPOTHETICAL;
|
|
|
|
if (verified) {
|
|
/* Use the strongest passing result */
|
|
for (int run = 0; run < 3; run++) {
|
|
if (results[run].passed && results[run].strength > final_strength)
|
|
final_strength = results[run].strength;
|
|
}
|
|
}
|
|
|
|
/* Retry logic — feedback-driven based on failure type */
|
|
int max_retries = 2;
|
|
int retry = 0;
|
|
while (!verified && retry < max_retries) {
|
|
tardy_failure_type_t fail = TARDY_FAIL_NONE;
|
|
/* Get failure type from the best failing result */
|
|
for (int run = 0; run < 3; run++) {
|
|
if (!results[run].passed && results[run].failure_type != TARDY_FAIL_NONE) {
|
|
fail = results[run].failure_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fail == TARDY_FAIL_ONTOLOGY_GAP) {
|
|
/* Can't retry — ontology doesn't have the data */
|
|
break;
|
|
} else if (fail == TARDY_FAIL_DECOMPOSITION) {
|
|
/* Just break for now — decomposition is deterministic */
|
|
break;
|
|
} else if (fail == TARDY_FAIL_LOW_CONFIDENCE) {
|
|
/* Lower the threshold slightly for retry */
|
|
tardy_semantics_t retry_sem = *sem;
|
|
retry_sem.truth.min_confidence *= 0.9f;
|
|
/* Re-run pipeline with relaxed threshold */
|
|
pass_count = 0;
|
|
total_confidence = 0.0f;
|
|
for (int run = 0; run < 3; run++) {
|
|
tardy_decomposition_t run_decomps[3];
|
|
memset(run_decomps, 0, sizeof(run_decomps));
|
|
for (int d = 0; d < 3; d++)
|
|
run_decomps[d] = decomps[(d + run) % 3];
|
|
results[run] = tardy_pipeline_verify(
|
|
claim_buf, claim_len,
|
|
run_decomps, 3, &grounding, &consistency,
|
|
&work_log, &spec, &retry_sem);
|
|
if (results[run].passed) {
|
|
pass_count++;
|
|
total_confidence += results[run].confidence;
|
|
}
|
|
}
|
|
verified = (pass_count >= 2);
|
|
if (verified) {
|
|
avg_confidence = total_confidence / (float)pass_count;
|
|
for (int run = 0; run < 3; run++)
|
|
if (results[run].passed && results[run].strength > final_strength)
|
|
final_strength = results[run].strength;
|
|
}
|
|
} else {
|
|
/* Other failures — don't retry blindly */
|
|
break;
|
|
}
|
|
retry++;
|
|
}
|
|
|
|
work_log.agents_spawned = 3; /* 3 independent verification runs */
|
|
work_log.compute_ns = mcp_now_ns() - verify_start;
|
|
|
|
/* If passed, record provenance and freeze agent to @verified */
|
|
if (verified) {
|
|
/* Record verification results in provenance before freezing */
|
|
target->provenance.reason = "verified_claim";
|
|
|
|
/* Also record the number of grounded triples */
|
|
target->provenance.causality_count = grounding.grounded;
|
|
|
|
tardy_vm_freeze(srv->vm, target->id, TARDY_TRUST_VERIFIED);
|
|
|
|
/* Self-growing ontology: add verified triples so future
|
|
* claims can be grounded against past verifications.
|
|
* The more you verify, the more the system knows. */
|
|
if (srv->self_ontology_loaded) {
|
|
for (int t = 0; t < triple_count; t++) {
|
|
tardy_self_ontology_add(&srv->self_ontology,
|
|
all_triples[t].subject,
|
|
all_triples[t].predicate,
|
|
all_triples[t].object);
|
|
}
|
|
/* Re-evaluate Datalog to derive new facts from additions */
|
|
tardy_dl_evaluate(&srv->self_ontology.datalog);
|
|
}
|
|
|
|
/* Rule mining: learn patterns from verified claims */
|
|
if (triple_count >= 2) {
|
|
tardy_inference_learn(&srv->ruleset,
|
|
all_triples, triple_count);
|
|
}
|
|
|
|
char turn_msg[256];
|
|
snprintf(turn_msg, sizeof(turn_msg),
|
|
"verified: strength=%d confidence=%d%%",
|
|
(int)final_strength, (int)(avg_confidence * 100));
|
|
tardy_vm_converse(srv->vm, target->id, "agent", turn_msg);
|
|
} else {
|
|
tardy_vm_converse(srv->vm, target->id, "agent",
|
|
"verification failed");
|
|
}
|
|
|
|
/* Determine failure type string for response */
|
|
const char *fail_str = "none";
|
|
if (!verified) {
|
|
/* Find the dominant failure type from results */
|
|
tardy_failure_type_t dominant_fail = TARDY_FAIL_NONE;
|
|
for (int run = 0; run < 3; run++) {
|
|
if (!results[run].passed && results[run].failure_type != TARDY_FAIL_NONE) {
|
|
dominant_fail = results[run].failure_type;
|
|
break;
|
|
}
|
|
}
|
|
switch (dominant_fail) {
|
|
case TARDY_FAIL_DECOMPOSITION: fail_str = "decomposition_error"; break;
|
|
case TARDY_FAIL_ONTOLOGY_GAP: fail_str = "ontology_gap"; break;
|
|
case TARDY_FAIL_CONTRADICTION: fail_str = "contradiction"; break;
|
|
case TARDY_FAIL_LOW_CONFIDENCE: fail_str = "low_confidence"; break;
|
|
case TARDY_FAIL_INCONSISTENCY: fail_str = "inconsistency"; break;
|
|
case TARDY_FAIL_NO_EVIDENCE: fail_str = "no_evidence"; break;
|
|
case TARDY_FAIL_PROTOCOL: fail_str = "protocol_error"; break;
|
|
case TARDY_FAIL_LAZINESS: fail_str = "laziness"; break;
|
|
case TARDY_FAIL_AMBIGUITY: fail_str = "ambiguity"; break;
|
|
case TARDY_FAIL_CROSS_REP: fail_str = "cross_rep_conflict"; break;
|
|
default: fail_str = "none"; break;
|
|
}
|
|
}
|
|
|
|
/* Ontology status */
|
|
const char *ont_status = srv->bridge_connected ? "connected" :
|
|
(srv->self_ontology.triple_count > 0 ? "self-hosted" : "offline");
|
|
|
|
/* Build result message */
|
|
char result_text[512];
|
|
snprintf(result_text, sizeof(result_text),
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"verified=%s strength=%d confidence=%d%% "
|
|
"bft=%d/3 triples_grounded=%d/%d "
|
|
"failure=%s retries=%d ontology=%s\"}]}",
|
|
verified ? "true" : "false",
|
|
(int)final_strength,
|
|
(int)(avg_confidence * 100),
|
|
pass_count,
|
|
grounding.grounded,
|
|
grounding.count,
|
|
fail_str,
|
|
retry,
|
|
ont_status);
|
|
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, result_text);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: send_message ---- */
|
|
if (strcmp(tool_name, "send_message") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int from_tok = tardy_json_find(parser, args_tok, "agent_from");
|
|
int to_tok = tardy_json_find(parser, args_tok, "agent_to");
|
|
int pay_tok = tardy_json_find(parser, args_tok, "payload");
|
|
if (from_tok < 0 || to_tok < 0 || pay_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602,
|
|
"missing agent_from, agent_to, or payload");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char from_name[64], to_name[64], pay_text[TARDY_MAX_PAYLOAD];
|
|
tardy_json_str(parser, from_tok, from_name, sizeof(from_name));
|
|
tardy_json_str(parser, to_tok, to_name, sizeof(to_name));
|
|
tardy_json_str(parser, pay_tok, pay_text, sizeof(pay_text));
|
|
|
|
/* Find sender and receiver agents */
|
|
tardy_agent_t *sender = NULL;
|
|
tardy_agent_t *receiver = NULL;
|
|
for (int i = 0; i < srv->vm->agent_count && (!sender || !receiver); i++) {
|
|
if (!sender) {
|
|
tardy_agent_t *c = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, from_name);
|
|
if (c) sender = c;
|
|
}
|
|
if (!receiver) {
|
|
tardy_agent_t *c = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, to_name);
|
|
if (c) receiver = c;
|
|
}
|
|
}
|
|
|
|
if (!sender || !receiver) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "sender or receiver not found");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
size_t pay_len = strlen(pay_text) + 1;
|
|
int sent = tardy_vm_send(srv->vm, sender->id, receiver->id,
|
|
pay_text, pay_len, TARDY_TYPE_STR);
|
|
if (sent == 0) {
|
|
const char *ok_result =
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"message sent\"}]}";
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, ok_result);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
} else {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "send failed (inbox full?)");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: read_inbox ---- */
|
|
if (strcmp(tool_name, "read_inbox") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int agent_tok = tardy_json_find(parser, args_tok, "agent");
|
|
if (agent_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing agent");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char agent_name[64];
|
|
tardy_json_str(parser, agent_tok, agent_name, sizeof(agent_name));
|
|
|
|
/* Find the agent */
|
|
tardy_agent_t *target = NULL;
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *c = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, agent_name);
|
|
if (c) { target = c; break; }
|
|
}
|
|
|
|
if (!target) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "agent not found");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
tardy_message_t msg;
|
|
int got = tardy_vm_recv(srv->vm, target->id, &msg);
|
|
if (got == 0) {
|
|
/* Build result with payload and sender info */
|
|
char result_text[2048];
|
|
/* Escape payload for JSON */
|
|
char escaped[TARDY_MAX_PAYLOAD * 2];
|
|
int ei = 0;
|
|
for (size_t pi = 0; pi < msg.payload_len && msg.payload[pi]; pi++) {
|
|
if (msg.payload[pi] == '"' || msg.payload[pi] == '\\') {
|
|
if (ei < (int)sizeof(escaped) - 2)
|
|
escaped[ei++] = '\\';
|
|
}
|
|
if (ei < (int)sizeof(escaped) - 1)
|
|
escaped[ei++] = msg.payload[pi];
|
|
}
|
|
escaped[ei] = '\0';
|
|
|
|
snprintf(result_text, sizeof(result_text),
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"from=%016llx%016llx payload=%s\"}]}",
|
|
(unsigned long long)msg.from.hi,
|
|
(unsigned long long)msg.from.lo,
|
|
escaped);
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, result_text);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
} else {
|
|
const char *empty_result =
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"inbox empty\"}]}";
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, empty_result);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: set_semantics ---- */
|
|
if (strcmp(tool_name, "set_semantics") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char aname[64], key[64], val_str[64];
|
|
int at = tardy_json_find(parser, args_tok, "agent");
|
|
int kt = tardy_json_find(parser, args_tok, "key");
|
|
int vt = tardy_json_find(parser, args_tok, "value");
|
|
if (at < 0 || kt < 0 || vt < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing agent, key, or value");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
tardy_json_str(parser, at, aname, sizeof(aname));
|
|
tardy_json_str(parser, kt, key, sizeof(key));
|
|
tardy_json_str(parser, vt, val_str, sizeof(val_str));
|
|
|
|
/* Find agent */
|
|
tardy_agent_t *target = NULL;
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
target = tardy_vm_find_by_name(srv->vm, srv->vm->agents[i].id, aname);
|
|
if (target) break;
|
|
}
|
|
if (!target) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "agent not found");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
/* Clone from global if needed, then set key */
|
|
const tardy_semantics_t *current = tardy_vm_get_semantics(srv->vm, target->id);
|
|
tardy_semantics_t updated = *current;
|
|
|
|
if (strcmp(key, "truth.min_confidence") == 0) {
|
|
/* Simple float parse */
|
|
float f = 0.0f;
|
|
const char *s = val_str;
|
|
int neg = 0, i = 0;
|
|
if (s[0] == '-') { neg = 1; i = 1; }
|
|
for (; s[i] && s[i] != '.'; i++) f = f * 10.0f + (s[i] - '0');
|
|
if (s[i] == '.') { i++; float frac = 0.1f; for (; s[i]; i++) { f += (s[i] - '0') * frac; frac *= 0.1f; } }
|
|
if (neg) f = -f;
|
|
updated.truth.min_confidence = f;
|
|
} else if (strcmp(key, "truth.min_consensus_agents") == 0) {
|
|
updated.truth.min_consensus_agents = (int)tardy_json_int(parser, vt);
|
|
} else if (strcmp(key, "truth.min_evidence_triples") == 0) {
|
|
updated.truth.min_evidence_triples = (int)tardy_json_int(parser, vt);
|
|
} else if (strcmp(key, "pipeline.min_passing_layers") == 0) {
|
|
updated.pipeline.min_passing_layers = (int)tardy_json_int(parser, vt);
|
|
} else {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "unknown semantics key");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
tardy_vm_set_semantics(srv->vm, target->id, &updated);
|
|
|
|
const char *ok_result =
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"semantics updated\"}]}";
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE, id, ok_result);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: query_agents ---- */
|
|
if (strcmp(tool_name, "query_agents") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int query_tok = tardy_json_find(parser, args_tok, "query");
|
|
if (query_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing query");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char query_text[256];
|
|
tardy_json_str(parser, query_tok, query_text, sizeof(query_text));
|
|
|
|
tardy_query_result_t qresults[TARDY_MAX_QUERY_RESULTS];
|
|
int total_found = 0;
|
|
|
|
for (int qi = 0; qi < srv->vm->agent_count && total_found < TARDY_MAX_QUERY_RESULTS; qi++) {
|
|
tardy_agent_t *qa = &srv->vm->agents[qi];
|
|
if (qa->state == TARDY_STATE_DEAD || qa->context.child_count == 0)
|
|
continue;
|
|
tardy_query_result_t batch[TARDY_MAX_QUERY_RESULTS];
|
|
int found = tardy_vm_query(srv->vm, qa->id, query_text,
|
|
batch, TARDY_MAX_QUERY_RESULTS - total_found);
|
|
for (int qj = 0; qj < found; qj++) {
|
|
int dup = 0;
|
|
for (int qk = 0; qk < total_found; qk++) {
|
|
if (qresults[qk].agent_id.hi == batch[qj].agent_id.hi &&
|
|
qresults[qk].agent_id.lo == batch[qj].agent_id.lo) {
|
|
dup = 1;
|
|
if (batch[qj].score > qresults[qk].score)
|
|
qresults[qk].score = batch[qj].score;
|
|
break;
|
|
}
|
|
}
|
|
if (!dup && total_found < TARDY_MAX_QUERY_RESULTS)
|
|
qresults[total_found++] = batch[qj];
|
|
}
|
|
}
|
|
|
|
char result_buf[2048];
|
|
int rpos = 0;
|
|
const char *rpre = "{\"content\":[{\"type\":\"text\",\"text\":\"{\\\"results\\\":[";
|
|
int rplen = (int)strlen(rpre);
|
|
memcpy(result_buf + rpos, rpre, rplen);
|
|
rpos += rplen;
|
|
for (int qi = 0; qi < total_found; qi++) {
|
|
if (qi > 0 && rpos < (int)sizeof(result_buf) - 1)
|
|
result_buf[rpos++] = ',';
|
|
char entry[128];
|
|
int score_int = (int)(qresults[qi].score * 100);
|
|
int elen = snprintf(entry, sizeof(entry),
|
|
"{\\\"name\\\":\\\"%s\\\",\\\"score\\\":%d.%02d}",
|
|
qresults[qi].name, score_int / 100, score_int % 100);
|
|
if (elen > 0 && rpos + elen < (int)sizeof(result_buf)) {
|
|
memcpy(result_buf + rpos, entry, elen);
|
|
rpos += elen;
|
|
}
|
|
}
|
|
const char *rsuf = "]}\"}]}";
|
|
int rslen = (int)strlen(rsuf);
|
|
if (rpos + rslen < (int)sizeof(result_buf)) {
|
|
memcpy(result_buf + rpos, rsuf, rslen);
|
|
rpos += rslen;
|
|
}
|
|
result_buf[rpos] = '\0';
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE, id, result_buf);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: get_conversation ---- */
|
|
if (strcmp(tool_name, "get_conversation") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int agent_tok = tardy_json_find(parser, args_tok, "agent");
|
|
if (agent_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing agent");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char agent_name[64];
|
|
tardy_json_str(parser, agent_tok, agent_name, sizeof(agent_name));
|
|
|
|
/* Find the agent */
|
|
tardy_agent_t *conv_target = NULL;
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *c = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, agent_name);
|
|
if (c) { conv_target = c; break; }
|
|
}
|
|
|
|
if (!conv_target) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, "agent not found");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
|
|
tardy_conversation_turn_t turns[TARDY_MAX_CONVERSATION];
|
|
int count = tardy_vm_get_conversation(srv->vm, conv_target->id,
|
|
turns, TARDY_MAX_CONVERSATION);
|
|
|
|
/* Build JSON array of turns */
|
|
char result_buf[4096];
|
|
int rpos = 0;
|
|
const char *rpre = "{\"content\":[{\"type\":\"text\",\"text\":\"[";
|
|
int rplen = (int)strlen(rpre);
|
|
memcpy(result_buf + rpos, rpre, rplen);
|
|
rpos += rplen;
|
|
|
|
for (int ci = 0; ci < count; ci++) {
|
|
if (ci > 0 && rpos < (int)sizeof(result_buf) - 1)
|
|
result_buf[rpos++] = ',';
|
|
char entry[640];
|
|
int elen = snprintf(entry, sizeof(entry),
|
|
"{\\\"role\\\":\\\"%s\\\","
|
|
"\\\"content\\\":\\\"%s\\\","
|
|
"\\\"at\\\":%llu}",
|
|
turns[ci].role,
|
|
turns[ci].content,
|
|
(unsigned long long)turns[ci].at);
|
|
if (elen > 0 && rpos + elen < (int)sizeof(result_buf)) {
|
|
memcpy(result_buf + rpos, entry, elen);
|
|
rpos += elen;
|
|
}
|
|
}
|
|
|
|
const char *rsuf = "]\"}]}";
|
|
int rslen = (int)strlen(rsuf);
|
|
if (rpos + rslen < (int)sizeof(result_buf)) {
|
|
memcpy(result_buf + rpos, rsuf, rslen);
|
|
rpos += rslen;
|
|
}
|
|
result_buf[rpos] = '\0';
|
|
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, result_buf);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Built-in: load_ontology ---- */
|
|
if (strcmp(tool_name, "load_ontology") == 0) {
|
|
int args_tok = tardy_json_find(parser, params_tok, "arguments");
|
|
if (args_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing arguments");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
int path_tok = tardy_json_find(parser, args_tok, "path");
|
|
if (path_tok < 0) {
|
|
int elen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32602, "missing path");
|
|
if (elen > 0) mcp_write(srv->write_buf, elen);
|
|
return -1;
|
|
}
|
|
char ttl_path[256];
|
|
tardy_json_str(parser, path_tok, ttl_path, sizeof(ttl_path));
|
|
|
|
int loaded = tardy_self_ontology_load_ttl(&srv->self_ontology,
|
|
ttl_path);
|
|
char result_text[256];
|
|
if (loaded >= 0) {
|
|
srv->self_ontology_loaded = true;
|
|
snprintf(result_text, sizeof(result_text),
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"loaded %d triples from %s\"}]}",
|
|
loaded, ttl_path);
|
|
} else {
|
|
snprintf(result_text, sizeof(result_text),
|
|
"{\"content\":[{\"type\":\"text\","
|
|
"\"text\":\"failed to load %s\"}]}",
|
|
ttl_path);
|
|
}
|
|
int rlen = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, result_text);
|
|
if (rlen > 0) mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
/* Find agent by name anywhere in the tree */
|
|
tardy_agent_t *found_agent = NULL;
|
|
tardy_read_result_t full_result;
|
|
memset(&full_result, 0, sizeof(full_result));
|
|
full_result.status = TARDY_READ_HASH_MISMATCH;
|
|
char read_buf[512];
|
|
memset(read_buf, 0, sizeof(read_buf));
|
|
|
|
for (int i = 0; i < srv->vm->agent_count; i++) {
|
|
tardy_agent_t *candidate = tardy_vm_find_by_name(
|
|
srv->vm, srv->vm->agents[i].id, tool_name);
|
|
if (candidate) {
|
|
found_agent = candidate;
|
|
full_result = tardy_vm_read_full(srv->vm, srv->vm->agents[i].id,
|
|
tool_name, read_buf,
|
|
sizeof(read_buf));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (full_result.status == TARDY_READ_OK && found_agent) {
|
|
char result[2048];
|
|
int rlen = 0;
|
|
const char *pre = "{\"content\":[{\"type\":\"text\",\"text\":\"";
|
|
int prelen = (int)strlen(pre);
|
|
memcpy(result + rlen, pre, prelen);
|
|
rlen += prelen;
|
|
|
|
/* Format value based on type */
|
|
if (found_agent->type_tag == TARDY_TYPE_STR ||
|
|
found_agent->type_tag == TARDY_TYPE_ERROR) {
|
|
/* String: copy directly (escape quotes) */
|
|
const char *s = read_buf;
|
|
while (*s && rlen < (int)sizeof(result) - 10) {
|
|
if (*s == '"' || *s == '\\')
|
|
result[rlen++] = '\\';
|
|
result[rlen++] = *s++;
|
|
}
|
|
} else if (found_agent->type_tag == TARDY_TYPE_BOOL) {
|
|
int64_t bval;
|
|
memcpy(&bval, read_buf, sizeof(int64_t));
|
|
const char *bs = bval ? "true" : "false";
|
|
int bslen = (int)strlen(bs);
|
|
memcpy(result + rlen, bs, bslen);
|
|
rlen += bslen;
|
|
} else {
|
|
/* Int/Float: format as number string */
|
|
int64_t val;
|
|
memcpy(&val, read_buf, sizeof(int64_t));
|
|
char numstr[32];
|
|
int nlen = 0;
|
|
int neg = 0;
|
|
int64_t v = val;
|
|
if (v < 0) { neg = 1; v = -v; }
|
|
if (v == 0) numstr[nlen++] = '0';
|
|
while (v > 0) { numstr[nlen++] = '0' + (char)(v % 10); v /= 10; }
|
|
if (neg) numstr[nlen++] = '-';
|
|
for (int j = 0; j < nlen / 2; j++) {
|
|
char t = numstr[j];
|
|
numstr[j] = numstr[nlen - 1 - j];
|
|
numstr[nlen - 1 - j] = t;
|
|
}
|
|
memcpy(result + rlen, numstr, nlen);
|
|
rlen += nlen;
|
|
}
|
|
|
|
/* Close content array, then append _tardy, then close object.
|
|
* Current result so far: {"content":[{"type":"text","text":"VALUE
|
|
* We need: ...VALUE"}], "_tardy":{...}}
|
|
*/
|
|
const char *arr_close = "\"}]";
|
|
int aclen = (int)strlen(arr_close);
|
|
if (rlen + aclen < (int)sizeof(result)) {
|
|
memcpy(result + rlen, arr_close, aclen);
|
|
rlen += aclen;
|
|
}
|
|
|
|
/* Append provenance */
|
|
const char *prov_ont = srv->bridge_connected ? "connected" :
|
|
(srv->self_ontology.triple_count > 0 ? "self-hosted" : "offline");
|
|
int new_rlen = append_provenance(result, rlen, (int)sizeof(result),
|
|
&full_result, prov_ont);
|
|
if (new_rlen > 0)
|
|
rlen = new_rlen;
|
|
|
|
/* Close outer object */
|
|
if (rlen < (int)sizeof(result) - 1)
|
|
result[rlen++] = '}';
|
|
result[rlen] = '\0';
|
|
|
|
int len = build_response(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, result);
|
|
if (len > 0)
|
|
mcp_write(srv->write_buf, len);
|
|
} else {
|
|
tardy_read_status_t status = full_result.status;
|
|
const char *err;
|
|
switch (status) {
|
|
case TARDY_READ_HASH_MISMATCH: err = "hash verification failed"; break;
|
|
case TARDY_READ_NO_CONSENSUS: err = "no Byzantine consensus"; break;
|
|
case TARDY_READ_SIG_INVALID: err = "signature invalid"; break;
|
|
default: err = "read failed"; break;
|
|
}
|
|
int len = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id, -32000, err);
|
|
if (len > 0) mcp_write(srv->write_buf, len);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ============================================
|
|
* MCP Server Core
|
|
* ============================================ */
|
|
|
|
int tardy_mcp_init(tardy_mcp_server_t *srv, tardy_vm_t *vm)
|
|
{
|
|
if (!srv || !vm)
|
|
return -1;
|
|
memset(srv, 0, sizeof(tardy_mcp_server_t));
|
|
srv->vm = vm;
|
|
srv->running = 1;
|
|
|
|
/* Try connecting to ontology engines */
|
|
int bridge_ok = tardy_bridge_init(&srv->bridge,
|
|
"/tmp/tardygrada-ontology-sketch.sock",
|
|
"/tmp/tardygrada-ontology-complete.sock");
|
|
srv->bridge_connected = (bridge_ok == 0);
|
|
|
|
/* Log which ontology mode is active (stderr to avoid corrupting MCP) */
|
|
if (srv->bridge.dual_mode) {
|
|
/* both sketch and complete connected */
|
|
const char *msg = "[tardygrada] ontology: dual mode (sketch+complete)\n";
|
|
tardy_write(STDERR_FILENO, msg, strlen(msg));
|
|
} else if (srv->bridge.sketch.connected) {
|
|
/* sketch only */
|
|
const char *msg = "[tardygrada] ontology: sketch only\n";
|
|
tardy_write(STDERR_FILENO, msg, strlen(msg));
|
|
} else if (srv->bridge.complete.connected) {
|
|
/* complete only */
|
|
const char *msg = "[tardygrada] ontology: complete only\n";
|
|
tardy_write(STDERR_FILENO, msg, strlen(msg));
|
|
} else {
|
|
/* no ontology — fallback to UNKNOWN */
|
|
const char *msg = "[tardygrada] ontology: none (fallback to UNKNOWN)\n";
|
|
tardy_write(STDERR_FILENO, msg, strlen(msg));
|
|
}
|
|
|
|
/* Initialize self-hosted ontology (always available as fallback) */
|
|
tardy_self_ontology_init(&srv->self_ontology, vm);
|
|
srv->self_ontology_loaded = srv->self_ontology.initialized;
|
|
|
|
/* Initialize frame registry for CRDT merge */
|
|
tardy_frames_init(&srv->frames);
|
|
|
|
/* Initialize inference ruleset with synthetic backbone */
|
|
tardy_inference_init(&srv->ruleset);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int tardy_mcp_handle(tardy_mcp_server_t *srv, const char *request, int len)
|
|
{
|
|
tardy_json_parser_t parser;
|
|
if (tardy_json_parse(&parser, request, len) < 0)
|
|
return -1;
|
|
|
|
/* Extract method and id */
|
|
int method_tok = tardy_json_find(&parser, 0, "method");
|
|
int id_tok = tardy_json_find(&parser, 0, "id");
|
|
|
|
char id_str[32] = "null";
|
|
if (id_tok >= 0) {
|
|
if (parser.tokens[id_tok].type == TARDY_JSON_STRING) {
|
|
id_str[0] = '"';
|
|
tardy_json_str(&parser, id_tok, id_str + 1, sizeof(id_str) - 2);
|
|
int slen = (int)strlen(id_str);
|
|
id_str[slen] = '"';
|
|
id_str[slen + 1] = '\0';
|
|
} else if (parser.tokens[id_tok].type == TARDY_JSON_NUMBER) {
|
|
memcpy(id_str, parser.tokens[id_tok].start,
|
|
parser.tokens[id_tok].len);
|
|
id_str[parser.tokens[id_tok].len] = '\0';
|
|
}
|
|
}
|
|
|
|
if (method_tok < 0)
|
|
return -1;
|
|
|
|
/* Route to handler */
|
|
if (tardy_json_eq(&parser, method_tok, "initialize"))
|
|
return handle_initialize(srv, id_str);
|
|
|
|
if (tardy_json_eq(&parser, method_tok, "notifications/initialized"))
|
|
return 0; /* notification, no response */
|
|
|
|
if (tardy_json_eq(&parser, method_tok, "tools/list"))
|
|
return handle_tools_list(srv, id_str);
|
|
|
|
if (tardy_json_eq(&parser, method_tok, "tools/call"))
|
|
return handle_tools_call(srv, &parser, id_str);
|
|
|
|
/* Unknown method */
|
|
int rlen = build_error(srv->write_buf, TARDY_MCP_BUF_SIZE,
|
|
id_str, -32601, "method not found");
|
|
if (rlen > 0)
|
|
mcp_write(srv->write_buf, rlen);
|
|
return 0;
|
|
}
|
|
|
|
int tardy_mcp_run(tardy_mcp_server_t *srv)
|
|
{
|
|
while (srv->running) {
|
|
/* Read Content-Length header */
|
|
char hdr[128];
|
|
int hpos = 0;
|
|
int content_length = -1;
|
|
|
|
/* Read header line by line */
|
|
while (hpos < (int)sizeof(hdr) - 1) {
|
|
ssize_t n = read(STDIN_FILENO, hdr + hpos, 1);
|
|
if (n <= 0) {
|
|
srv->running = 0;
|
|
return 0; /* EOF — clean shutdown */
|
|
}
|
|
hpos++;
|
|
|
|
/* Check for \r\n\r\n (end of headers) */
|
|
if (hpos >= 4 &&
|
|
hdr[hpos - 4] == '\r' && hdr[hpos - 3] == '\n' &&
|
|
hdr[hpos - 2] == '\r' && hdr[hpos - 1] == '\n') {
|
|
|
|
/* Parse Content-Length */
|
|
hdr[hpos] = '\0';
|
|
const char *cl = "Content-Length: ";
|
|
char *found = NULL;
|
|
for (int i = 0; i < hpos - 15; i++) {
|
|
if (memcmp(hdr + i, cl, 16) == 0) {
|
|
found = hdr + i + 16;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
content_length = 0;
|
|
while (*found >= '0' && *found <= '9') {
|
|
content_length = content_length * 10 + (*found - '0');
|
|
found++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (content_length <= 0 || content_length >= TARDY_MCP_BUF_SIZE)
|
|
continue;
|
|
|
|
/* Read body */
|
|
int total = 0;
|
|
while (total < content_length) {
|
|
ssize_t n = read(STDIN_FILENO, srv->read_buf + total,
|
|
content_length - total);
|
|
if (n <= 0) {
|
|
srv->running = 0;
|
|
return 0;
|
|
}
|
|
total += (int)n;
|
|
}
|
|
srv->read_buf[content_length] = '\0';
|
|
|
|
/* Handle the request */
|
|
tardy_mcp_handle(srv, srv->read_buf, content_length);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void tardy_mcp_stop(tardy_mcp_server_t *srv)
|
|
{
|
|
if (srv) {
|
|
srv->running = 0;
|
|
tardy_bridge_shutdown(&srv->bridge);
|
|
}
|
|
}
|