vcon-server

Connect an AI agent over MCP

Give an AI agent its own key, then check the MCP endpoint by hand: the handshake and the tool list over HTTPS, the refusals, and the same through vcon-server mcp.

An AI agent reaches vcon-server through the Model Context Protocol (MCP): over HTTPS at /mcp on the server's TLS listener, or by starting vcon-server mcp, which forwards to a running server. MCP describes both, and the configuration an agent's client reads. This how-to makes a key for an agent and checks each path by hand, with curl and with vcon-server mcp, before an agent is pointed at it.

The tools the server lists are the ones the key's scopes allow; MCP gives every tool and the scope it needs. The key this page makes holds only read, so its tool list has the two tools that read vCons.

CI runs every command on this page against a live server, after getting started, and checks each answer.

Before you start

  • A running server, set up as in getting started, with the tenant acme, and the certificate's copy for clients at /usr/local/share/vcon-server/ca.pem.
  • One shell for every step: later steps use variables that earlier steps set.
URL=https://vcon.example.com:8443
CONFIG=/etc/vcon-server/vcon-server.toml
TENANT=$(sudo -u vcon vcon-server tenant list --config $CONFIG | jq -r 'select(.name == "acme") | .id')

Give the agent its own key

An agent is a client like any other: give it a key of its own, with only the scopes its work needs and a label that names it. Over MCP the key reaches what it reaches over HTTP, no more:

AGENT=$(sudo -u vcon vcon-server key create --tenant "$TENANT" --scope read --label agent-1 \
  --config $CONFIG | jq -r .token)

The handshake over HTTPS

Each request to POST /mcp carries one JSON-RPC message, with the key as a bearer token, and gets one JSON answer. initialize names the protocol version the client speaks. The server answers with its own name and version:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/mcp" \
  -H "Authorization: Bearer $AGENT" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | jq -c '{protocol: .result.protocolVersion, server: .result.serverInfo.name}'
{"protocol":"2025-06-18","server":"vcon-server"}

The tool list names the tools this key may call, here the two that need only read:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/mcp" \
  -H "Authorization: Bearer $AGENT" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq -c '[.result.tools[].name]'
["vcon_get","vcon_list"]

What the endpoint refuses

Without a key, or with a revoked one, the answer is 401. A request with an Origin header, as a web page sends, is refused with 403. GET /mcp is 405: there is no event stream:

BODY='{"jsonrpc":"2.0","id":3,"method":"tools/list"}'
H=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/mcp" "${H[@]}" -d "$BODY"
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/mcp" "${H[@]}" -d "$BODY" \
  -H "Authorization: Bearer $AGENT" -H "Origin: https://example.com"
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/mcp" -H "Authorization: Bearer $AGENT"
401
403
405

The same through vcon-server mcp

An agent that starts its MCP servers as commands runs vcon-server mcp. It reads a client file that names the server's /mcp address, a file holding the key, and the certificate to trust. Make them in a directory readable by the agent's account only:

install -d -m 0700 agent
printf '%s\n' "$AGENT" > agent/agent.token
chmod 0600 agent/agent.token
cp /usr/local/share/vcon-server/ca.pem agent/ca.pem
cat > agent/mcp.toml <<EOF
url = "$URL/mcp"
key_file = "$PWD/agent/agent.token"
ca = "$PWD/agent/ca.pem"
EOF

The command reads one JSON-RPC message a line on standard input and writes each answer on standard output, one a line. It stops when standard input ends. Send the handshake and the tool list:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"shell","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | vcon-server mcp --config agent/mcp.toml \
  | jq -c 'if .result.tools then [.result.tools[].name] else {server: .result.serverInfo.name} end'
{"server":"vcon-server"}
["vcon_get","vcon_list"]

Like every vcon-server command, mcp locks its memory, so the account that runs it needs the memory-lock allowance (see getting started). A key the server refuses stops it with exit code 1:

printf 'vcs_0000000000000000_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' > agent/agent.token
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"shell","version":"1"}}}' \
  | vcon-server mcp --config agent/mcp.toml
vcon-server: the server refused the key (401)

Every request is in the audit log

Each request to /mcp is counted in the key's tenant log under the action mcp, with its status. Read the statuses with a key that holds audit. Here they are 200 for the answers, 202 for the notification, and 403 for the request with an Origin header:

AUDITOR=$(sudo -u vcon vcon-server key create --tenant "$TENANT" --scope audit --label agent-audit \
  --config $CONFIG | jq -r .token)
curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/audit?limit=10000" -H "Authorization: Bearer $AUDITOR" \
  | jq -r 'select(.action == "mcp") | .status' | sort -u
200
202
403

Point the agent at it

MCP gives the configuration blocks for a client that starts vcon-server mcp and for one that connects over HTTP itself. Copy agent/mcp.toml, the key file and ca.pem to the agent's account, and point the client file's paths at them.

Clean up

Revoke the keys this page made:

sudo -u vcon vcon-server key list --config $CONFIG | jq -r 'select(.label | startswith("agent-")) | .id' \
  | while read -r id; do sudo -u vcon vcon-server key revoke "$id" --config $CONFIG; done
rm -r agent

Source of this page: website/content/docs/connect-mcp.md