100% free and open source · every tool, every host, no licence key · read the licence
Secronyx

Reference

JSON-RPC API

The MCP JSON-RPC 2.0 methods Secronyx serves, its error codes and result cap, and the HTTP transport's endpoints, headers, authentication challenge and CORS rules.

Secronyx speaks the Model Context Protocol, which is JSON-RPC 2.0 over one of two transports: newline-delimited messages on stdin and stdout, or HTTP POST. The protocol version it reports is 2024-11-05. One message handler serves both transports; the HTTP transport adds authentication, rate limiting, CORS and a few side endpoints. This page documents exactly what the server accepts and returns, taken from internal/mcp/protocol.go, server.go and http.go. AI clients normally drive this for you; see Connect an AI client. The tools themselves are catalogued in the Tool reference.

Message envelope

A request is a JSON object with jsonrpc, id, method and optional params. The server applies these rules before looking at the method:

  • The message must parse as a single JSON object. Anything else, including a JSON-RPC batch array or an empty body, yields -32700 Parse error with the parser's message in data and no id.
  • jsonrpc must be exactly "2.0", otherwise -32600 Invalid Request with data: "jsonrpc must be 2.0".
  • A message without an id is treated as a notification and is never answered, whatever its method. A tools/call sent without an id executes nothing and returns nothing. Always include an id (string or number) on calls you expect a reply to.
  • An unknown method on a request with an id yields -32601 Method not found with the method name in data.

On stdio each message is one line. The reader accepts lines up to 10 MiB and skips blank lines. Responses are written one per line. On HTTP the whole request body is one message and the response body is one message.

Methods

initialize

Establishes the session. params is optional; when present it must decode into protocolVersion, capabilities and clientInfo, otherwise -32602 Invalid params. The server does not negotiate: it always answers with its own protocol version and the tools capability.

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"example-client","version":"1.0"}}}
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"secronyx","version":"dev"}}}

serverInfo.version is the build version set at link time; a local build reports dev.

initialized

Sent by the client as a notification once initialize has succeeded; the server records that the session is initialized. If a client sends it with an id, the server answers with an empty success, {"jsonrpc":"2.0","id":9}, where result is omitted because it is null. notifications/cancelled is accepted as a notification and ignored.

ping

Liveness check with no parameters. The result is an empty object.

{"jsonrpc":"2.0","id":2,"method":"ping"}
{"jsonrpc":"2.0","id":2,"result":{}}

tools/list

Returns the tools the caller may invoke. There are no parameters and no pagination. On stdio, or on HTTP without authentication, this is every registered tool. On HTTP with an authenticated identity, the list is filtered to the tools whose scope the identity's grants cover, so a client only sees what it can call.

{"jsonrpc":"2.0","id":3,"method":"tools/list"}
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      {
        "name": "get_cpu_info",
        "description": "Get CPU usage, frequency, load average, and core count",
        "inputSchema": {
          "type": "object",
          "properties": {
            "per_cpu": {
              "type": "boolean",
              "description": "Include per-CPU core statistics",
              "default": false
            }
          }
        }
      }
    ]
  }
}

Each tool is {name, description, inputSchema}. inputSchema is a JSON Schema object with type (always object), optional properties and optional required. Each property carries type and may carry description, enum, default, minimum and maximum. Nothing else appears in a schema, so clients that understand only this subset are safe.

With the default policy the server lists 521 tools; --enable-sensitive adds 9 more and --scopes core,logs reduces the list to 13. The Tool reference lists them all.

tools/call

Executes one tool. params is {"name": string, "arguments": object}; arguments may be omitted. params that do not decode yield -32602 Invalid params.

{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_uptime","arguments":{}}}
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"boot_time\": \"2026-08-27T23:55:40.247146919+01:00\",\n  \"uptime\": 1760957200000000,\n  \"uptime_str\": \"20 days, 9 hours, 9 minutes, 17 seconds\",\n  \"timestamp\": \"2026-09-17T09:04:57.447152169+01:00\"\n}"
      }
    ]
  }
}

The result is {content: [...], isError?: bool}. Every tool returns one content item of type: "text" whose text is the collector's result serialised as indented JSON; parse text to get structured data. The Content type also defines mimeType and data fields for image and resource content, but no tool handler produces them.

The server distinguishes four outcomes:

Outcome What you get Audit result
Tool not registered JSON-RPC error -32602, message Tool not found, data is the name error, tool not found
Caller's scopes do not cover the tool JSON-RPC error -32003, message Forbidden, data is tool "<name>" requires scope "<scope>" denied, scope denied
Collector returned an error JSON-RPC result with isError: true and one text item Error: <message> error, the message
Result exceeds the cap JSON-RPC result with isError: true and text Error: result too large: <n> bytes exceeds the <cap> byte cap; narrow the query (limit, lines, filters) error, result_too_large

Note the split: a tool that does not exist or that the caller may not use is a protocol-level error; a tool that ran and failed is a successful RPC carrying an error result, which is how MCP expects model-visible failures to be reported. Many collectors also report partial failure inside their JSON (an error field alongside empty data) rather than failing the call; asking get_npm_lock for a path outside the allowlist, for example, returns "error": "path is outside the allowed directories" in the text with isError unset.

The result cap defaults to 4 MiB (4,194,304 bytes of text summed across content items) and is set with --max-result-bytes; values below 1 KiB are raised to 1 KiB. Tools that page or filter (limit, lines and similar arguments) are the intended way to stay under it.

Each tools/call is timed and written to the audit log with the tool name, arguments, identity, client IP, duration and outcome; see Audit logging.

Error codes

Code Message When
-32700 Parse error The body is not a single JSON object. No id is returned.
-32600 Invalid Request jsonrpc is not "2.0".
-32601 Method not found Unknown method; data is the method name.
-32602 Invalid params params failed to decode for initialize or tools/call.
-32602 Tool not found tools/call named a tool that is not registered; data is the name.
-32003 Forbidden The authenticated caller lacks the tool's scope; data says which scope. This sits in the implementation-defined server error range.
-32603 Internal error Defined in protocol.go; no current code path emits it.

Error responses have the shape {"jsonrpc":"2.0","id":<id>,"error":{"code":<n>,"message":"<text>","data":<any>}}; data is omitted when empty.

{"jsonrpc":"2.0","id":5,"error":{"code":-32003,"message":"Forbidden","data":"tool \"get_env_vars\" requires scope \"sensitive\""}}

Scope checks at request time

When the HTTP transport authenticates a caller it places an identity with a list of granted scopes in the request context. A grant covers a tool when it is the wildcard *, the tool's scope name itself (core), or the namespaced form mcp:tools:<scope> (mcp:tools:core) for authorization servers whose scope namespace is shared with other resources. The bare mcp:tools grants nothing. A static bearer token is granted *; API keys, SSH keys, mTLS identities and JWTs carry explicit lists. Without an identity (stdio, or HTTP with no authentication configured) the only control is which tools were registered at startup. See Scopes and authorization.

HTTP transport

Start it with --transport http; the listener, TLS and authentication flags are on the Configuration reference. All paths are served from the root of the listener.

Method and path Authentication Purpose
POST / Required when configured The MCP endpoint. The body is one JSON-RPC message.
GET /.well-known/oauth-protected-resource None RFC 9728 protected-resource metadata.
GET /health, HEAD /health None Liveness. Returns {"status":"ok"} with Cache-Control: no-store and deliberately nothing about the configuration.
GET /metrics Required when configured Prometheus metrics. Labels name every tool called, and a counter tracks authentication successes and failures, so this endpoint gets the same authentication as /.

OPTIONS is answered by the CORS layer described below. Any other method on /, /health or the metadata path, including GET /, returns 405 Method Not Allowed with a plain-text body; /metrics does not check the method. There is no streaming or Server-Sent Events endpoint: each request is one message and one response.

Request and response

The MCP endpoint reads the entire body and hands it to the same handler as stdio. The request Content-Type is not checked. The response always carries Content-Type: application/json. The HTTP status is 200 for every JSON-RPC response, including JSON-RPC errors; only transport-level failures use other statuses. A notification (no id) returns 200 with an empty body. If the body cannot be read, the status is 400 with {"error":"Failed to read request body"}. The handler applies no explicit body-size limit; the server's 30 s read timeout bounds the upload, and SSH-signed requests are additionally limited to 1 MiB of body because that is what the signature covers.

curl -sS -X POST https://mcp.example.com/ \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_memory_info","arguments":{}}}'

Authentication headers

The request is authenticated before the body is read. The credential chain is consulted in order and the first method that recognises a credential of its kind decides; a rejected credential is a denial and later methods are not tried. The Authentication page covers each method in depth.

Order Method Header or mechanism
1 API key (--api-keys-file) X-API-Key: msk_<id>_<secret> or Authorization: ApiKey msk_<id>_<secret>
2 SSH signature (--ssh-authorized-keys, --ssh-ca-keys) Authorization: SSH-Sig keyid="SHA256:...", ts="<unix seconds>", nonce="<base64url>", sig="<base64>" with an optional cert="<base64>" field
3 Mutual TLS (--tls-client-ca) The verified client certificate on the TLS connection; no header.
4 Static token, SaaS JWKS, OIDC or introspection, whichever is configured Authorization: Bearer <token>

Scheme names are matched case-insensitively. An SSH-signed request must be sent exactly as signed (method, request URI, lower-cased Host, body); secronyx ssh-sign, on the Command line page, produces the header or a complete curl invocation.

A missing or rejected credential produces 401 Unauthorized:

HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="secronyx", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

{"error":"unauthorized","error_description":"missing Authorization header"}

error_description is the authenticator's message, prefixed with the method name when a chain entry rejected the credential (for example api-key: malformed API key). The resource_metadata URL is built from --server-url. On success, the identity's subject, client id, scopes and method are written to the audit log as a token_validation success event; failures are written as denials with the error text.

Client address

The client IP recorded in audit events and used for rate limiting, API-key CIDR pinning and SSH from= restrictions is the TCP peer address. With --trust-proxy-headers it is instead the first entry of X-Forwarded-For, or X-Real-IP when that header is absent. Enable this only behind a proxy that overwrites those headers.

Rate limiting and lockout

With --rate-limit (the default) a middleware runs outermost, before authentication, on every path:

  • Per-address token bucket: 20 requests per second sustained with a burst of 40. Exceeding it returns 429 Too Many Requests with a Retry-After header in whole seconds and the body rate limit exceeded.
  • Global concurrency cap of 32 in-flight requests. When full, the response is 503 Service Unavailable with Retry-After: 1 and the body server busy.
  • Brute-force lockout: 10 responses with status 401 to one address within 15 minutes locks that address out for 15 minutes; requests during the lockout receive 429 with Retry-After set to the remaining time.

Rejections are answered before any authentication or tool code runs.

CORS and browser origins

By default the server sends no CORS headers, so browsers refuse to read cross-origin responses and a page on any site cannot probe a developer's loopback server. --cors-origin lists the origins to permit:

  • An origin is reflected only on an exact, case-insensitive match with the request's Origin header, and Vary: Origin is added.
  • The entry * is honoured only when no authentication is configured. With authentication on it is silently ignored, because a wildcard would let any page relay a victim's bearer token.
  • When an origin is allowed, the response carries Access-Control-Allow-Origin, Access-Control-Allow-Methods: GET, POST, OPTIONS and Access-Control-Allow-Headers: Authorization, Content-Type. Access-Control-Allow-Credentials is never set.
  • Every OPTIONS request is answered 204 No Content before authentication, carrying the CORS headers only if the origin matched.
OPTIONS / HTTP/1.1
Origin: https://app.example.com

HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Protected resource metadata

GET /.well-known/oauth-protected-resource returns RFC 9728 metadata so OAuth-aware clients can discover where to obtain a token. It is cacheable for an hour (Cache-Control: public, max-age=3600).

{
  "resource": "https://mcp.example.com",
  "resource_name": "Secronyx Server",
  "resource_description": "Read-only AI diagnostics plane for secure incident triage",
  "scopes_supported": ["alerts","analytics","compliance","consumer","core","enhanced","hardware","hooks","logs","network","report","resources","security","software","state","storage","triage","windows"],
  "authorization_servers": ["https://enterprise.okta.com"]
}

scopes_supported is computed from the tools actually registered, so it never advertises a scope the server cannot honour; sensitive appears only when --enable-sensitive is set. authorization_servers holds the OIDC issuer when --oidc-issuer is set, the introspection server when --auth-server is set, and is absent otherwise.

A complete session over stdio

secronyx --scopes core <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}
{"jsonrpc":"2.0","method":"initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_uptime"}}
EOF

Three lines come back on stdout: the initialize result, the eight-tool core list, and the uptime result. The initialized notification produces no output. Log lines go to stderr, and audit events go to /var/log/secronyx/audit.jsonl, or to stderr if that file cannot be opened.

Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.