2026-10-18 20:06:11 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// MCP endpoint: Streamable HTTP on POST /mcp, a lean implementation of our
|
|
|
|
|
// own of the JSON-RPC 2.0 subset that MCP actually needs (deliberately NO
|
|
|
|
|
// SDK dependency; the protocol scope here is four methods plus ping).
|
|
|
|
|
// Semantics per MCP 2025-06-18, with two documented simplifications the
|
|
|
|
|
// transport allows:
|
2026-10-18 20:06:11 +02:00
|
|
|
//
|
2026-10-24 11:15:35 +02:00
|
|
|
// - Stateless: every POST is independent; there are no session IDs
|
|
|
|
|
// (Mcp-Session-Id is ignored) and therefore no enforcement of the
|
|
|
|
|
// initialize-first rule; a curl can call tools/call directly. MCP
|
|
|
|
|
// clients do not mind: they run the handshake voluntarily.
|
|
|
|
|
// - No server-initiated streams: talkd never pushes (the tool list is
|
|
|
|
|
// static). GET /mcp therefore answers 405, the response the Streamable
|
|
|
|
|
// HTTP transport specifies for servers without an SSE stream.
|
2026-10-18 20:06:11 +02:00
|
|
|
//
|
2026-10-24 11:15:35 +02:00
|
|
|
// Responses: requests get 200 application/json (JSON-RPC error objects also
|
|
|
|
|
// 200; the HTTP part did succeed); notifications get 202 without a body
|
|
|
|
|
// (JSON-RPC 2.0). Batches (arrays) are no longer part of MCP since
|
|
|
|
|
// 2025-06-18 and yield -32600.
|
2026-10-18 20:06:11 +02:00
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpProtocolVersion is the version talkd itself speaks when the client
|
|
|
|
|
// names none (initialize without protocolVersion). Requested versions are
|
|
|
|
|
// accepted and echoed back; what is implemented is the 2025-06-18 scope
|
|
|
|
|
// documented above.
|
2026-10-18 20:06:11 +02:00
|
|
|
const mcpProtocolVersion = "2025-06-18"
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// JSON-RPC 2.0 error codes (subset in use).
|
2026-10-18 20:06:11 +02:00
|
|
|
const (
|
|
|
|
|
rpcParseError = -32700
|
|
|
|
|
rpcInvalidRequest = -32600
|
|
|
|
|
rpcMethodNotFound = -32601
|
|
|
|
|
rpcInvalidParams = -32602
|
|
|
|
|
rpcInternalError = -32603
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpServerName is the serverInfo name in the initialize result.
|
2026-10-18 20:06:11 +02:00
|
|
|
const mcpServerName = "talkd"
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// jsonRPCRequest is an incoming JSON-RPC message. ID as RawMessage keeps
|
|
|
|
|
// "field missing" (notification) distinguishable from id:0 or id:string.
|
2026-10-18 20:06:11 +02:00
|
|
|
type jsonRPCRequest struct {
|
|
|
|
|
JSONRPC string `json:"jsonrpc"`
|
|
|
|
|
ID json.RawMessage `json:"id"`
|
|
|
|
|
Method string `json:"method"`
|
|
|
|
|
Params json.RawMessage `json:"params"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// jsonRPCResponse is an outgoing JSON-RPC message. Exactly one of
|
|
|
|
|
// Result/Error is set. ID is never omitted; null on parse errors without a
|
|
|
|
|
// known ID (JSON-RPC 2.0).
|
2026-10-18 20:06:11 +02:00
|
|
|
type jsonRPCResponse struct {
|
|
|
|
|
JSONRPC string `json:"jsonrpc"`
|
|
|
|
|
ID json.RawMessage `json:"id"`
|
|
|
|
|
Result any `json:"result,omitempty"`
|
|
|
|
|
Error *rpcError `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// rpcError is the error object of a JSON-RPC response.
|
2026-10-18 20:06:11 +02:00
|
|
|
type rpcError struct {
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpInitializeParams holds the relevant parts of the initialize params
|
|
|
|
|
// (client capabilities do not interest talkd; tools suffice).
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpInitializeParams struct {
|
|
|
|
|
ProtocolVersion string `json:"protocolVersion"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpServerInfo describes the server in the initialize result.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpServerInfo struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpCapabilities reports in the initialize result what talkd offers:
|
|
|
|
|
// tools (speak), nothing else.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpCapabilities struct {
|
|
|
|
|
Tools map[string]any `json:"tools"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpInitializeResult is the result of initialize: the (echoed) protocol
|
|
|
|
|
// version, the capabilities and the server info, in 2025-06-18 form.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpInitializeResult struct {
|
|
|
|
|
ProtocolVersion string `json:"protocolVersion"`
|
|
|
|
|
Capabilities mcpCapabilities `json:"capabilities"`
|
|
|
|
|
ServerInfo mcpServerInfo `json:"serverInfo"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpToolsListResult is the result of tools/list: exactly one tool (speak);
|
|
|
|
|
// no nextCursor (the list always fits on one page).
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpToolsListResult struct {
|
|
|
|
|
Tools []mcpTool `json:"tools"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpTool is a tool entry in tools/list.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpTool struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
InputSchema mcpSchema `json:"inputSchema"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpSchema is the JSON Schema object of a tool input.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpSchema struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Properties map[string]mcpProperty `json:"properties"`
|
|
|
|
|
Required []string `json:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpProperty is a single schema property.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpProperty struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Description string `json:"description,omitempty"`
|
|
|
|
|
Enum []string `json:"enum,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpToolCallParams are the parameters of tools/call.
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpToolCallParams struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Arguments json.RawMessage `json:"arguments"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpContent is a content block of a tool result (here: text).
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpContent struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpToolResult is the result of tools/call. isError:false (omitted) also
|
|
|
|
|
// for muted/discarded: that is a RESULT of admission, not a protocol error
|
|
|
|
|
// (deliberate decision, see speakToolResult).
|
2026-10-18 20:06:11 +02:00
|
|
|
type mcpToolResult struct {
|
|
|
|
|
Content []mcpContent `json:"content"`
|
|
|
|
|
IsError bool `json:"isError,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// serveMCP handles POST /mcp: one JSON-RPC message per request.
|
2026-10-18 20:06:11 +02:00
|
|
|
func (h *handler) serveMCP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBodyBytes))
|
|
|
|
|
if err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
writeRPCError(w, nil, rpcParseError, "reading body: "+err.Error())
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(strings.TrimLeft(string(body), " \t\r\n"), "[") {
|
|
|
|
|
writeRPCError(w, nil, rpcInvalidRequest,
|
2026-10-24 11:15:35 +02:00
|
|
|
"batch requests are not supported (MCP 2025-06-18 removed batches); one message per POST")
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req jsonRPCRequest
|
|
|
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
writeRPCError(w, nil, rpcParseError, "invalid JSON: "+err.Error())
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if req.JSONRPC != "2.0" {
|
|
|
|
|
writeRPCError(w, nil, rpcInvalidRequest,
|
2026-10-24 11:15:35 +02:00
|
|
|
`jsonrpc must be "2.0" (got `+strconv.Quote(req.JSONRPC)+`)`)
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if isNotification(req.ID) {
|
2026-10-24 11:15:35 +02:00
|
|
|
// Notifications (notifications/initialized, ...) never get a
|
|
|
|
|
// JSON-RPC response, only the transport's 202. Unknown
|
|
|
|
|
// notifications are silently ignored (JSON-RPC 2.0).
|
2026-10-18 20:06:11 +02:00
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch req.Method {
|
|
|
|
|
case "initialize":
|
|
|
|
|
h.mcpInitialize(w, req)
|
|
|
|
|
case "notifications/initialized":
|
2026-10-24 11:15:35 +02:00
|
|
|
// Sent with an ID: not a regular client; the correct answer is an
|
|
|
|
|
// empty result, not an error.
|
2026-10-18 20:06:11 +02:00
|
|
|
writeRPCResult(w, req.ID, struct{}{})
|
|
|
|
|
case "ping":
|
|
|
|
|
writeRPCResult(w, req.ID, struct{}{})
|
|
|
|
|
case "tools/list":
|
|
|
|
|
writeRPCResult(w, req.ID, mcpToolsListResult{Tools: []mcpTool{speakTool()}})
|
|
|
|
|
case "tools/call":
|
|
|
|
|
h.mcpToolsCall(w, req)
|
|
|
|
|
default:
|
|
|
|
|
writeRPCError(w, req.ID, rpcMethodNotFound,
|
2026-10-24 11:15:35 +02:00
|
|
|
"method "+strconv.Quote(req.Method)+" is not implemented (available: initialize, notifications/initialized, ping, tools/list, tools/call)")
|
2026-10-18 20:06:11 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpGet handles GET /mcp: 405 with Allow. talkd offers no server-initiated
|
|
|
|
|
// streams (the Streamable HTTP response for that case); there are no tool
|
|
|
|
|
// changes.
|
2026-10-18 20:06:11 +02:00
|
|
|
func (h *handler) mcpGet(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Allow", "POST, OPTIONS")
|
|
|
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpInitialize handles initialize: the requested protocol version is
|
|
|
|
|
// echoed (accepted); empty means our own (2025-06-18). Capabilities: tools;
|
|
|
|
|
// serverInfo: talkd + daemon version.
|
2026-10-18 20:06:11 +02:00
|
|
|
func (h *handler) mcpInitialize(w http.ResponseWriter, req jsonRPCRequest) {
|
|
|
|
|
var params mcpInitializeParams
|
|
|
|
|
if len(req.Params) > 0 {
|
|
|
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
writeRPCError(w, req.ID, rpcInvalidParams, "initialize params: "+err.Error())
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
version := params.ProtocolVersion
|
|
|
|
|
if version == "" {
|
|
|
|
|
version = mcpProtocolVersion
|
|
|
|
|
}
|
|
|
|
|
writeRPCResult(w, req.ID, mcpInitializeResult{
|
|
|
|
|
ProtocolVersion: version,
|
|
|
|
|
Capabilities: mcpCapabilities{Tools: map[string]any{}},
|
|
|
|
|
ServerInfo: mcpServerInfo{Name: mcpServerName, Version: h.version},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// mcpToolsCall handles tools/call: exactly one tool (speak) whose arguments
|
|
|
|
|
// take the same admission path as POST /speak, including mute and the laws
|
|
|
|
|
// (rate limit/quiet hours).
|
2026-10-18 20:06:11 +02:00
|
|
|
func (h *handler) mcpToolsCall(w http.ResponseWriter, req jsonRPCRequest) {
|
|
|
|
|
var params mcpToolCallParams
|
|
|
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
writeRPCError(w, req.ID, rpcInvalidParams, "tools/call params: "+err.Error())
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if params.Name != "speak" {
|
|
|
|
|
writeRPCError(w, req.ID, rpcInvalidParams,
|
2026-10-24 11:15:35 +02:00
|
|
|
"unknown tool "+strconv.Quote(params.Name)+" (available: speak)")
|
2026-10-18 20:06:11 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args := params.Arguments
|
|
|
|
|
if len(args) == 0 {
|
|
|
|
|
args = json.RawMessage("{}")
|
|
|
|
|
}
|
|
|
|
|
var speakReq SpeakRequest
|
|
|
|
|
if err := json.Unmarshal(args, &speakReq); err != nil {
|
|
|
|
|
writeRPCError(w, req.ID, rpcInvalidParams, "arguments: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
adm := h.admitSpeak(speakReq)
|
|
|
|
|
if adm.status != http.StatusAccepted {
|
2026-10-24 11:15:35 +02:00
|
|
|
// Validation/internal error (HTTP equivalent 400/503): report as a
|
|
|
|
|
// JSON-RPC error; this was not an admission result.
|
2026-10-18 20:06:11 +02:00
|
|
|
code := rpcInvalidParams
|
|
|
|
|
if adm.status != http.StatusBadRequest {
|
|
|
|
|
code = rpcInternalError
|
|
|
|
|
}
|
|
|
|
|
writeRPCError(w, req.ID, code, adm.errMsg)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeRPCResult(w, req.ID, speakToolResult(adm.resp))
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// speakTool is the tool description of speak: input schema with text
|
|
|
|
|
// required; priority/provider/lang/now optional.
|
2026-10-18 20:06:11 +02:00
|
|
|
func speakTool() mcpTool {
|
|
|
|
|
return mcpTool{
|
|
|
|
|
Name: "speak",
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "Speaks a short announcement (status, next step, what the user is needed for; " +
|
|
|
|
|
"never long answers). Goes through the daemon: queue, rate limit, mute and quiet hours apply.",
|
2026-10-18 20:06:11 +02:00
|
|
|
InputSchema: mcpSchema{
|
|
|
|
|
Type: "object",
|
|
|
|
|
Properties: map[string]mcpProperty{
|
|
|
|
|
"text": {
|
|
|
|
|
Type: "string",
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "The announcement text (1 to 2 sentences by etiquette)",
|
2026-10-18 20:06:11 +02:00
|
|
|
},
|
|
|
|
|
"priority": {
|
|
|
|
|
Type: "string",
|
|
|
|
|
Enum: []string{"low", "normal", "high"},
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "Processing priority (default normal)",
|
2026-10-18 20:06:11 +02:00
|
|
|
},
|
|
|
|
|
"provider": {
|
|
|
|
|
Type: "string",
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "Provider pin (elevenlabs, openai, edge, osbuiltin); empty = default chain",
|
2026-10-18 20:06:11 +02:00
|
|
|
},
|
|
|
|
|
"lang": {
|
|
|
|
|
Type: "string",
|
|
|
|
|
Enum: []string{"de", "en"},
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "Language pin; empty = automatic detection",
|
2026-10-18 20:06:11 +02:00
|
|
|
},
|
|
|
|
|
"now": {
|
|
|
|
|
Type: "boolean",
|
2026-10-24 11:15:35 +02:00
|
|
|
Description: "Jumps ahead of the queue and deliberately bypasses rate limit and quiet hours",
|
2026-10-18 20:06:11 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Required: []string{"text"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// speakToolResult translates a /speak admission response into the MCP tool
|
|
|
|
|
// result. Mute and law discards are deliberately NOT isError: they are the
|
|
|
|
|
// correct, documented result of admission (no protocol error); the agent
|
|
|
|
|
// should read the reason, not crash. Successful admission: "queued: <id>".
|
2026-10-18 20:06:11 +02:00
|
|
|
func speakToolResult(resp SpeakResponse) mcpToolResult {
|
|
|
|
|
switch {
|
|
|
|
|
case resp.Muted:
|
|
|
|
|
return mcpToolResult{Content: []mcpContent{{
|
|
|
|
|
Type: "text",
|
2026-10-24 11:15:35 +02:00
|
|
|
Text: "muted: announcement discarded (mute is on)",
|
2026-10-18 20:06:11 +02:00
|
|
|
}}}
|
|
|
|
|
case resp.Discarded == queue.DiscardRateLimit:
|
|
|
|
|
return mcpToolResult{Content: []mcpContent{{
|
|
|
|
|
Type: "text",
|
2026-10-24 11:15:35 +02:00
|
|
|
Text: "discarded: rate_limit; announcement discarded (priority low over the window budget)",
|
2026-10-18 20:06:11 +02:00
|
|
|
}}}
|
|
|
|
|
case resp.Discarded == queue.DiscardQuietHours:
|
2026-10-24 11:15:35 +02:00
|
|
|
text := "discarded: quiet_hours; quiet hours active, announcement discarded"
|
2026-10-18 20:06:11 +02:00
|
|
|
if resp.Toast {
|
2026-10-24 11:15:35 +02:00
|
|
|
text += " (replaced by a toast, no audio)"
|
2026-10-18 20:06:11 +02:00
|
|
|
}
|
|
|
|
|
return mcpToolResult{Content: []mcpContent{{Type: "text", Text: text}}}
|
|
|
|
|
case resp.Discarded != "":
|
|
|
|
|
return mcpToolResult{Content: []mcpContent{{
|
|
|
|
|
Type: "text",
|
|
|
|
|
Text: "discarded: " + resp.Discarded,
|
|
|
|
|
}}}
|
|
|
|
|
default:
|
2026-10-24 09:54:40 +02:00
|
|
|
text := "queued: " + resp.ID
|
|
|
|
|
if resp.Truncated {
|
2026-10-24 11:15:35 +02:00
|
|
|
text += " (text truncated to max_text; announcements are 1 to 2 sentences, not long answers)"
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
return mcpToolResult{Content: []mcpContent{{Type: "text", Text: text}}}
|
2026-10-18 20:06:11 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// isNotification reports whether the ID marks a notification (field missing
|
|
|
|
|
// or explicitly null; both get no response per JSON-RPC 2.0).
|
2026-10-18 20:06:11 +02:00
|
|
|
func isNotification(id json.RawMessage) bool {
|
|
|
|
|
return len(id) == 0 || strings.TrimSpace(string(id)) == "null"
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// writeRPCResult writes a successful JSON-RPC response (200,
|
2026-10-18 20:06:11 +02:00
|
|
|
// application/json).
|
|
|
|
|
func writeRPCResult(w http.ResponseWriter, id json.RawMessage, result any) {
|
|
|
|
|
writeRPC(w, jsonRPCResponse{JSONRPC: "2.0", ID: id, Result: result})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// writeRPCError writes a JSON-RPC error response; id null when the request
|
|
|
|
|
// ID is unknown (parse error).
|
2026-10-18 20:06:11 +02:00
|
|
|
func writeRPCError(w http.ResponseWriter, id json.RawMessage, code int, msg string) {
|
|
|
|
|
writeRPC(w, jsonRPCResponse{
|
|
|
|
|
JSONRPC: "2.0",
|
|
|
|
|
ID: id,
|
|
|
|
|
Error: &rpcError{Code: code, Message: msg},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// writeRPC writes the JSON-RPC response to the response writer.
|
2026-10-18 20:06:11 +02:00
|
|
|
func writeRPC(w http.ResponseWriter, resp jsonRPCResponse) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
|
|
|
}
|