feat: Session 10.3 MCP Server + Session 11 Graph Engine Deep Integration
MCP Server (10.3): - STDIO JSON-RPC 2.0 transport with 3 AI tools: query, vector_search, schema_inspect - Multi-tenant session vars (tenant_id, user_id) with RLS support - Standalone binary: build/baramcp - Tested with all 3 tools + parameterized queries + vector search + schema inspect Graph Engine Deep Integration (Session 11): - CREATE GRAPH / DROP GRAPH DDL support - Graph engine wired to SQL executor via GRAPH_TABLE() function - 7 algorithms: BFS, DFS, PageRank, ShortestPath, Dijkstra, Louvain, Community - INSERT into _nodes/_edges tables auto-syncs with native Graph adjacency lists - Optional MATCH pattern, ALGORITHM, START, END, MAXDEPTH in GRAPH_TABLE syntax - All 340+ existing tests pass
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
## BaraDB MCP Server — Model Context Protocol
|
||||
##
|
||||
## Implements the MCP (Model Context Protocol) over STDIO transport
|
||||
## with JSON-RPC 2.0. Provides AI agents with tools to query, vector
|
||||
## search, and inspect the BaraDB schema.
|
||||
##
|
||||
## Tools:
|
||||
## query — Execute SQL queries with parameterized inputs
|
||||
## vector_search — Semantic vector similarity search with tenant isolation
|
||||
## schema_inspect — Explore tables, columns, indexes, RLS policies
|
||||
|
||||
import std/json
|
||||
import std/strutils
|
||||
import std/os
|
||||
import std/tables
|
||||
import std/sequtils
|
||||
|
||||
import ../storage/lsm
|
||||
import ../query/lexer as qlexer
|
||||
import ../query/parser as qparser
|
||||
import ../query/ast
|
||||
import ../query/executor
|
||||
import ../protocol/wire
|
||||
import ../core/mvcc
|
||||
import ../fts/engine as fts
|
||||
import ../vector/engine as vengine
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP JSON-RPC 2.0 types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
JsonRpcErrorCode* = enum
|
||||
jrParseError = -32700
|
||||
jrInvalidRequest = -32600
|
||||
jrMethodNotFound = -32601
|
||||
jrInvalidParams = -32602
|
||||
jrInternalError = -32603
|
||||
|
||||
McpToolDef* = object
|
||||
name*: string
|
||||
description*: string
|
||||
inputSchema*: JsonNode
|
||||
|
||||
McpServerInfo* = object
|
||||
name*: string
|
||||
version*: string
|
||||
|
||||
McpServerCapabilities* = object
|
||||
tools*: JsonNode
|
||||
|
||||
# Tool definitions (lazy initialization)
|
||||
var toolDefs: seq[McpToolDef]
|
||||
|
||||
proc buildToolDefs() =
|
||||
if toolDefs.len > 0:
|
||||
return
|
||||
|
||||
toolDefs = @[
|
||||
McpToolDef(
|
||||
name: "query",
|
||||
description: "Execute a SQL query against BaraDB. Supports SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, and all BaraQL statements. Use parameterized queries with ? placeholders to prevent SQL injection. Returns rows as an array of objects keyed by column name.",
|
||||
inputSchema: %*{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The SQL query to execute. Use ? for parameterized values."
|
||||
},
|
||||
"params": {
|
||||
"type": "array",
|
||||
"description": "Optional parameter values for ? placeholders in the SQL query.",
|
||||
"items": {}
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string",
|
||||
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS filtering."
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "Optional. Sets the current user for RLS policy evaluation."
|
||||
}
|
||||
},
|
||||
"required": ["sql"]
|
||||
}
|
||||
),
|
||||
McpToolDef(
|
||||
name: "vector_search",
|
||||
description: "Perform semantic vector similarity search against a BaraDB HNSW vector index. Finds the k-nearest neighbors to a query vector. Supports tenant isolation via session variables. Results include distance scores and metadata.",
|
||||
inputSchema: %*{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"type": "string",
|
||||
"description": "The table name containing the vector column."
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
"description": "The vector column name with an HNSW index."
|
||||
},
|
||||
"query_vector": {
|
||||
"type": "array",
|
||||
"description": "The query vector as an array of floats.",
|
||||
"items": {"type": "number"}
|
||||
},
|
||||
"k": {
|
||||
"type": "integer",
|
||||
"description": "Number of nearest neighbors to return (default: 10).",
|
||||
"default": 10
|
||||
},
|
||||
"metric": {
|
||||
"type": "string",
|
||||
"enum": ["cosine", "euclidean", "dot_product", "manhattan"],
|
||||
"description": "Distance metric (default: cosine).",
|
||||
"default": "cosine"
|
||||
},
|
||||
"filter_column": {
|
||||
"type": "string",
|
||||
"description": "Optional metadata column to filter results."
|
||||
},
|
||||
"filter_value": {
|
||||
"type": "string",
|
||||
"description": "Value for filter_column. Only results matching this value are returned."
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string",
|
||||
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS filtering."
|
||||
}
|
||||
},
|
||||
"required": ["table", "column", "query_vector"]
|
||||
}
|
||||
),
|
||||
McpToolDef(
|
||||
name: "schema_inspect",
|
||||
description: "Explore and inspect the BaraDB database schema. Returns tables, columns, data types, primary keys, foreign keys, indexes (BTree, HNSW vector, full-text), and RLS policies. Optionally filter to a specific table.",
|
||||
inputSchema: %*{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"type": "string",
|
||||
"description": "Optional. If provided, returns detailed schema for only this table."
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string",
|
||||
"description": "Optional. Sets the app.tenant_id session variable for multi-tenant RLS context."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
McpServerCtx* = ref object
|
||||
db*: LSMTree
|
||||
execCtx*: ExecutionContext
|
||||
dataDir*: string
|
||||
|
||||
var serverCtx: McpServerCtx
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON-RPC helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
proc parseVectorFromJson(node: JsonNode): seq[float32] =
|
||||
result = @[]
|
||||
if node.kind == JArray:
|
||||
for item in node:
|
||||
case item.kind
|
||||
of JInt: result.add(float32(item.getInt()))
|
||||
of JFloat: result.add(float32(item.getFloat()))
|
||||
else: discard
|
||||
|
||||
proc parseMetric(s: string): vengine.DistanceMetric =
|
||||
case s.toLowerAscii()
|
||||
of "cosine": vengine.dmCosine
|
||||
of "euclidean": vengine.dmEuclidean
|
||||
of "dot_product", "dotproduct": vengine.dmDotProduct
|
||||
of "manhattan": vengine.dmManhattan
|
||||
else: vengine.dmCosine
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: query
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc handleQuery(params: JsonNode): JsonNode =
|
||||
if params.kind != JObject:
|
||||
return %*{"error": "params must be a JSON object"}
|
||||
|
||||
if "sql" notin params or params["sql"].kind != JString:
|
||||
return %*{"error": "Missing required parameter: sql (string)"}
|
||||
|
||||
let sql = params["sql"].getStr()
|
||||
if sql.strip().len == 0:
|
||||
return %*{"error": "SQL query cannot be empty"}
|
||||
|
||||
var prevTenant = serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||
var prevUser = serverCtx.execCtx.currentUser
|
||||
|
||||
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||
let tid = params["tenant_id"].getStr()
|
||||
if tid.len > 0:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = tid
|
||||
if "user_id" in params and params["user_id"].kind == JString:
|
||||
let uid = params["user_id"].getStr()
|
||||
if uid.len > 0:
|
||||
serverCtx.execCtx.currentUser = uid
|
||||
|
||||
var wireParams: seq[WireValue] = @[]
|
||||
if "params" in params and params["params"].kind == JArray:
|
||||
for p in params["params"]:
|
||||
case p.kind
|
||||
of JNull: wireParams.add(WireValue(kind: fkNull))
|
||||
of JBool: wireParams.add(WireValue(kind: fkBool, boolVal: p.getBool()))
|
||||
of JInt: wireParams.add(WireValue(kind: fkInt64, int64Val: p.getInt()))
|
||||
of JFloat: wireParams.add(WireValue(kind: fkFloat64, float64Val: p.getFloat()))
|
||||
of JString: wireParams.add(WireValue(kind: fkString, strVal: p.getStr()))
|
||||
else: wireParams.add(WireValue(kind: fkString, strVal: $p))
|
||||
|
||||
var tokens: seq[qlexer.Token]
|
||||
try:
|
||||
tokens = qlexer.tokenize(sql)
|
||||
except:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
return %*{"error": "Failed to tokenize SQL: " & getCurrentExceptionMsg()}
|
||||
|
||||
var astNode: Node
|
||||
try:
|
||||
astNode = qparser.parse(tokens)
|
||||
except:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
return %*{"error": "Failed to parse SQL: " & getCurrentExceptionMsg()}
|
||||
|
||||
if astNode.stmts.len == 0:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
return %*{"columns": [], "rows": [], "affectedRows": 0}
|
||||
|
||||
var res: ExecResult
|
||||
try:
|
||||
res = executeQuery(serverCtx.execCtx, astNode, wireParams)
|
||||
except:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
return %*{"error": "Query execution failed: " & getCurrentExceptionMsg()}
|
||||
|
||||
if not res.success:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
return %*{"error": res.message}
|
||||
|
||||
var jsonRows = newJArray()
|
||||
for row in res.rows:
|
||||
var jsonRow = newJObject()
|
||||
for col in res.columns:
|
||||
if col in row:
|
||||
jsonRow[col] = %row[col]
|
||||
else:
|
||||
jsonRow[col] = newJNull()
|
||||
jsonRows.add(jsonRow)
|
||||
|
||||
var jsonCols = newJArray()
|
||||
for c in res.columns:
|
||||
jsonCols.add(%c)
|
||||
|
||||
var r = %*{
|
||||
"columns": jsonCols,
|
||||
"rows": jsonRows,
|
||||
"affectedRows": res.affectedRows
|
||||
}
|
||||
if res.message.len > 0:
|
||||
r["message"] = %res.message
|
||||
|
||||
var sessionInfo = newJObject()
|
||||
sessionInfo["tenant_id"] = %serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||
r["_session"] = sessionInfo
|
||||
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = prevTenant
|
||||
serverCtx.execCtx.currentUser = prevUser
|
||||
|
||||
return r
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: vector_search
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc handleVectorSearch(params: JsonNode): JsonNode =
|
||||
if params.kind != JObject:
|
||||
return %*{"error": "params must be a JSON object"}
|
||||
|
||||
if "table" notin params or params["table"].kind != JString:
|
||||
return %*{"error": "Missing required parameter: table (string)"}
|
||||
if "column" notin params or params["column"].kind != JString:
|
||||
return %*{"error": "Missing required parameter: column (string)"}
|
||||
if "query_vector" notin params:
|
||||
return %*{"error": "Missing required parameter: query_vector (array of floats)"}
|
||||
|
||||
let table = params["table"].getStr()
|
||||
let column = params["column"].getStr()
|
||||
let indexKey = table & "." & column
|
||||
|
||||
if indexKey notin serverCtx.execCtx.vectorIndexes:
|
||||
var available: seq[string] = @[]
|
||||
for k in serverCtx.execCtx.vectorIndexes.keys:
|
||||
available.add(k)
|
||||
return %*{
|
||||
"error": "No vector index found for '" & indexKey & "'",
|
||||
"available_indexes": %available
|
||||
}
|
||||
|
||||
let idx = serverCtx.execCtx.vectorIndexes[indexKey]
|
||||
if idx.isNil or idx.nodes.len == 0:
|
||||
return %*{"error": "Vector index for '" & indexKey & "' is empty"}
|
||||
|
||||
let queryVec = parseVectorFromJson(params["query_vector"])
|
||||
if queryVec.len == 0:
|
||||
return %*{"error": "query_vector must be a non-empty array of numbers"}
|
||||
if queryVec.len != idx.dimensions:
|
||||
return %*{"error": "Vector dimension mismatch: got " & $queryVec.len &
|
||||
", expected " & $idx.dimensions}
|
||||
|
||||
let k = if "k" in params and params["k"].kind == JInt:
|
||||
params["k"].getInt()
|
||||
else: 10
|
||||
if k < 1 or k > 1000:
|
||||
return %*{"error": "k must be between 1 and 1000"}
|
||||
|
||||
let metric = if "metric" in params and params["metric"].kind == JString:
|
||||
parseMetric(params["metric"].getStr())
|
||||
else: vengine.dmCosine
|
||||
|
||||
var results: seq[(uint64, float64, Table[string, string])]
|
||||
|
||||
let hasFilter = "filter_column" in params and params["filter_column"].kind == JString and
|
||||
"filter_value" in params and params["filter_value"].kind == JString
|
||||
|
||||
if hasFilter:
|
||||
let filterCol = params["filter_column"].getStr()
|
||||
let filterVal = params["filter_value"].getStr()
|
||||
let filterFn = proc(metadata: Table[string, string]): bool {.gcsafe.} =
|
||||
result = filterCol in metadata and metadata[filterCol] == filterVal
|
||||
let rawResults = idx.searchWithFilter(queryVec, k, filterFn, metric)
|
||||
for (id, dist) in rawResults:
|
||||
var meta = initTable[string, string]()
|
||||
if id in idx.nodes:
|
||||
meta = idx.nodes[id].metadata
|
||||
results.add((id, dist, meta))
|
||||
else:
|
||||
results = idx.searchEx(queryVec, k, metric)
|
||||
|
||||
var jsonResults = newJArray()
|
||||
for (id, dist, meta) in results:
|
||||
var item = %*{
|
||||
"id": %id,
|
||||
"distance": dist
|
||||
}
|
||||
var metaObj = newJObject()
|
||||
for key, val in meta:
|
||||
metaObj[key] = %val
|
||||
item["metadata"] = metaObj
|
||||
jsonResults.add(item)
|
||||
|
||||
var sessionInfo = newJObject()
|
||||
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||
let tid = params["tenant_id"].getStr()
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = tid
|
||||
sessionInfo["tenant_id"] = %tid
|
||||
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||
|
||||
return %*{
|
||||
"table": %table,
|
||||
"column": %column,
|
||||
"index_size": idx.nodes.len,
|
||||
"k": k,
|
||||
"metric": $metric,
|
||||
"results": jsonResults,
|
||||
"_session": sessionInfo
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: schema_inspect
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc handleSchemaInspect(params: JsonNode): JsonNode =
|
||||
var targetTable = ""
|
||||
if params.kind == JObject and "table" in params and params["table"].kind == JString:
|
||||
targetTable = params["table"].getStr()
|
||||
|
||||
if "tenant_id" in params and params["tenant_id"].kind == JString:
|
||||
serverCtx.execCtx.sessionVars["app.tenant_id"] = params["tenant_id"].getStr()
|
||||
|
||||
var jsonTables = newJArray()
|
||||
|
||||
for tblName, tblDef in serverCtx.execCtx.tables:
|
||||
if targetTable.len > 0 and tblName != targetTable:
|
||||
continue
|
||||
|
||||
var jsonCols = newJArray()
|
||||
for col in tblDef.columns:
|
||||
var colObj = %*{
|
||||
"name": col.name,
|
||||
"type": col.colType,
|
||||
"primary_key": col.isPk,
|
||||
"not_null": col.isNotNull,
|
||||
"unique": col.isUnique,
|
||||
"auto_increment": col.autoIncrement
|
||||
}
|
||||
if col.defaultVal.len > 0:
|
||||
colObj["default"] = %col.defaultVal
|
||||
|
||||
var fkInfo: JsonNode = nil
|
||||
if col.fkTable.len > 0:
|
||||
fkInfo = %*{
|
||||
"table": col.fkTable,
|
||||
"column": col.fkColumn,
|
||||
"on_delete": col.fkOnDelete,
|
||||
"on_update": col.fkOnUpdate
|
||||
}
|
||||
colObj["foreign_key"] = fkInfo
|
||||
|
||||
jsonCols.add(colObj)
|
||||
|
||||
var jsonIdxs = newJArray()
|
||||
for key in serverCtx.execCtx.btrees.keys:
|
||||
if key.startsWith(tblName & ".") or key == tblName:
|
||||
jsonIdxs.add(%*{"type": "btree", "name": key})
|
||||
for key in serverCtx.execCtx.vectorIndexes.keys:
|
||||
if key.startsWith(tblName & "."):
|
||||
let vi = serverCtx.execCtx.vectorIndexes[key]
|
||||
jsonIdxs.add(%*{
|
||||
"type": "hnsw_vector",
|
||||
"name": key,
|
||||
"dimensions": vi.dimensions,
|
||||
"node_count": vi.nodes.len
|
||||
})
|
||||
for key in serverCtx.execCtx.ftsIndexes.keys:
|
||||
if key.startsWith(tblName & "."):
|
||||
let ftsIdx = serverCtx.execCtx.ftsIndexes[key]
|
||||
jsonIdxs.add(%*{"type": "fulltext", "name": key, "doc_count": ftsIdx.docCount})
|
||||
|
||||
var jsonPolicies = newJArray()
|
||||
if tblName in serverCtx.execCtx.policies:
|
||||
for pol in serverCtx.execCtx.policies[tblName]:
|
||||
jsonPolicies.add(%*{
|
||||
"name": pol.name,
|
||||
"command": pol.command
|
||||
})
|
||||
|
||||
var fks = newJArray()
|
||||
for fk in tblDef.foreignKeys:
|
||||
fks.add(%*{
|
||||
"table": fk.refTable,
|
||||
"column": fk.refColumn,
|
||||
"on_delete": fk.onDelete,
|
||||
"on_update": fk.onUpdate
|
||||
})
|
||||
|
||||
var tblObj = %*{
|
||||
"name": tblName,
|
||||
"columns": jsonCols,
|
||||
"primary_keys": %tblDef.pkColumns,
|
||||
"indexes": jsonIdxs,
|
||||
"foreign_keys": fks,
|
||||
"policies": jsonPolicies
|
||||
}
|
||||
jsonTables.add(tblObj)
|
||||
|
||||
if targetTable.len > 0 and jsonTables.len == 0:
|
||||
return %*{"error": "Table '" & targetTable & "' not found"}
|
||||
|
||||
var sessionInfo = newJObject()
|
||||
sessionInfo["tenant_id"] = %serverCtx.execCtx.sessionVars.getOrDefault("app.tenant_id", "")
|
||||
sessionInfo["user_id"] = %serverCtx.execCtx.currentUser
|
||||
|
||||
return %*{
|
||||
"tables": jsonTables,
|
||||
"table_count": jsonTables.len,
|
||||
"_session": sessionInfo
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP Protocol handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc handleInitialize(params: JsonNode): JsonNode =
|
||||
buildToolDefs()
|
||||
return %*{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"serverInfo": {
|
||||
"name": "BaraDB MCP Server",
|
||||
"version": "1.1.2"
|
||||
},
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
}
|
||||
}
|
||||
|
||||
proc handleToolsList(params: JsonNode): JsonNode =
|
||||
buildToolDefs()
|
||||
var tools = newJArray()
|
||||
for td in toolDefs:
|
||||
tools.add(%*{
|
||||
"name": td.name,
|
||||
"description": td.description,
|
||||
"inputSchema": td.inputSchema
|
||||
})
|
||||
return %*{"tools": tools}
|
||||
|
||||
proc handleToolsCall(params: JsonNode): JsonNode =
|
||||
if params.kind != JObject:
|
||||
return %*{"error": "params must be a JSON object"}
|
||||
|
||||
if "name" notin params or params["name"].kind != JString:
|
||||
return %*{"error": "Missing tool name"}
|
||||
|
||||
let toolName = params["name"].getStr()
|
||||
let toolArgs = if "arguments" in params: params["arguments"] else: newJObject()
|
||||
|
||||
var content: JsonNode
|
||||
case toolName
|
||||
of "query":
|
||||
content = handleQuery(toolArgs)
|
||||
of "vector_search":
|
||||
content = handleVectorSearch(toolArgs)
|
||||
of "schema_inspect":
|
||||
content = handleSchemaInspect(toolArgs)
|
||||
else:
|
||||
return %*{"error": "Unknown tool: " & toolName}
|
||||
|
||||
var text: string
|
||||
if content.hasKey("error"):
|
||||
text = "Error: " & content["error"].getStr()
|
||||
else:
|
||||
text = $content
|
||||
|
||||
return %*{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": text
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON-RPC dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc dispatch(meth: string, params: JsonNode): JsonNode =
|
||||
case meth
|
||||
of "initialize":
|
||||
return handleInitialize(params)
|
||||
of "tools/list":
|
||||
return handleToolsList(params)
|
||||
of "tools/call":
|
||||
return handleToolsCall(params)
|
||||
else:
|
||||
return %*{
|
||||
"error": {
|
||||
"code": jrMethodNotFound.int,
|
||||
"message": "Method not found: " & meth
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# STDIO transport
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc writeToStdout(line: string) =
|
||||
try:
|
||||
stdout.writeLine(line)
|
||||
stdout.flushFile()
|
||||
except:
|
||||
discard
|
||||
|
||||
proc logToStderr*(msg: string) =
|
||||
try:
|
||||
stderr.writeLine("[baradb-mcp] " & msg)
|
||||
stderr.flushFile()
|
||||
except:
|
||||
discard
|
||||
|
||||
proc processMessage(raw: string): string =
|
||||
if raw.strip().len == 0:
|
||||
return ""
|
||||
|
||||
var req: JsonNode
|
||||
try:
|
||||
req = parseJson(raw)
|
||||
except:
|
||||
logToStderr("JSON parse error: " & getCurrentExceptionMsg())
|
||||
let resp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"id": newJNull(),
|
||||
"error": {
|
||||
"code": jrParseError.int,
|
||||
"message": "Parse error: " & getCurrentExceptionMsg()
|
||||
}
|
||||
}
|
||||
return $resp
|
||||
|
||||
if req.kind != JObject:
|
||||
let resp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"id": newJNull(),
|
||||
"error": {
|
||||
"code": jrInvalidRequest.int,
|
||||
"message": "Invalid request: not a JSON object"
|
||||
}
|
||||
}
|
||||
return $resp
|
||||
|
||||
if "method" notin req or req["method"].kind != JString:
|
||||
let resp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"id": newJNull(),
|
||||
"error": {
|
||||
"code": jrInvalidRequest.int,
|
||||
"message": "Invalid request: missing method"
|
||||
}
|
||||
}
|
||||
return $resp
|
||||
|
||||
let meth = req["method"].getStr()
|
||||
let params = if "params" in req: req["params"] else: newJObject()
|
||||
|
||||
let isNotification = "id" notin req
|
||||
|
||||
if meth == "notifications/initialized":
|
||||
return ""
|
||||
|
||||
var dispResult: JsonNode
|
||||
try:
|
||||
dispResult = dispatch(meth, params)
|
||||
except:
|
||||
logToStderr("Dispatch error for " & meth & ": " & getCurrentExceptionMsg())
|
||||
let msg = getCurrentExceptionMsg()
|
||||
var errResp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": jrInternalError.int,
|
||||
"message": "Internal error: " & msg
|
||||
}
|
||||
}
|
||||
if not isNotification:
|
||||
errResp["id"] = req["id"]
|
||||
return $errResp
|
||||
|
||||
if isNotification:
|
||||
return ""
|
||||
|
||||
var resp: JsonNode
|
||||
if dispResult.hasKey("error"):
|
||||
var errNode = dispResult["error"]
|
||||
var errObj: JsonNode
|
||||
if errNode.kind == JObject:
|
||||
errObj = errNode
|
||||
else:
|
||||
errObj = %*{"code": jrInternalError.int, "message": errNode.getStr()}
|
||||
resp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req["id"],
|
||||
"error": errObj
|
||||
}
|
||||
else:
|
||||
resp = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req["id"],
|
||||
"result": dispResult
|
||||
}
|
||||
return $resp
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc init*(dataDir: string = "./data"): McpServerCtx =
|
||||
logToStderr("BaraDB MCP Server v1.1.2 initializing...")
|
||||
let db = newLSMTree(dataDir)
|
||||
let ctx = newExecutionContext(db)
|
||||
ctx.txnManager = newTxnManager()
|
||||
result = McpServerCtx(db: db, execCtx: ctx, dataDir: dataDir)
|
||||
serverCtx = result
|
||||
logToStderr("Initialized. Data directory: " & dataDir)
|
||||
|
||||
proc run*() =
|
||||
buildToolDefs()
|
||||
logToStderr("MCP Server ready. Waiting for JSON-RPC requests on STDIN...")
|
||||
logToStderr("Available tools: " & $toolDefs.mapIt(it.name))
|
||||
|
||||
var startupDone = false
|
||||
while true:
|
||||
var line = ""
|
||||
try:
|
||||
line = stdin.readLine()
|
||||
except EOFError:
|
||||
if startupDone:
|
||||
logToStderr("STDIN closed, exiting.")
|
||||
break
|
||||
except:
|
||||
logToStderr("STDIN read error: " & getCurrentExceptionMsg())
|
||||
break
|
||||
|
||||
let resp = processMessage(line)
|
||||
if resp.len > 0:
|
||||
writeToStdout(resp)
|
||||
if not startupDone:
|
||||
startupDone = true
|
||||
|
||||
proc close*() =
|
||||
if serverCtx != nil and serverCtx.db != nil:
|
||||
logToStderr("Closing database...")
|
||||
serverCtx.db.close()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Standalone entry helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc parseDataDir*(): string =
|
||||
result = getEnv("BARADB_DATA_DIR", "./data")
|
||||
var i = 1
|
||||
while i < paramCount():
|
||||
let arg = paramStr(i)
|
||||
if arg == "--data-dir" and i + 1 <= paramCount():
|
||||
result = paramStr(i + 1)
|
||||
break
|
||||
inc i
|
||||
|
||||
when isMainModule:
|
||||
let dataDir = parseDataDir()
|
||||
logToStderr("Starting BaraDB MCP Server with data dir: " & dataDir)
|
||||
try:
|
||||
discard init(dataDir)
|
||||
run()
|
||||
except:
|
||||
logToStderr("Fatal error: " & getCurrentExceptionMsg())
|
||||
finally:
|
||||
close()
|
||||
Reference in New Issue
Block a user