Skip to content

Model Context Protocol (MCP)

Sema includes a built-in Model Context Protocol server. This allows LLM clients (such as Claude Desktop, Cursor, or Claude Code) to inspect, compile, format, evaluate, and build Sema code in your host environment, as well as execute user-defined Lisp tools.

The server communicates over standard input/output (stdio) using JSON-RPC 2.0.

bash
sema mcp

Sema is also an MCP client — it can consume external MCP servers' tools (see Sema as an MCP client below).


Connecting Clients

Register sema mcp as a stdio server with your client of choice. With no file arguments it serves the default tools; append filepaths to any command below to also expose your own deftool definitions (see Filepath Mode).

Claude Code

bash
claude mcp add sema -- sema mcp

Registered for the current project by default; add --scope user to make it available everywhere.

Codex

bash
codex mcp add sema -- sema mcp

Written to ~/.codex/config.toml, equivalent to:

toml
[mcp_servers.sema]
command = "sema"
args = ["mcp"]

GitHub Copilot CLI

bash
copilot mcp add sema -- sema mcp

Written to ~/.copilot/mcp-config.json (user-wide). Copilot also picks up a project-local .mcp.json in the same mcpServers format shown below.

Gemini CLI

bash
gemini mcp add sema sema mcp

Registered for the current project by default (.gemini/settings.json); add -s user to make it available everywhere.

Amp

bash
amp mcp add sema -- sema mcp

Written to ~/.config/amp/settings.json; add --workspace for a project-local registration.

Glue

bash
glue mcp add sema --transport stdio -- sema mcp

Unlike the CLIs above, Glue requires an explicit --transport. Written to ~/.glue/config.yaml.

Cursor

Add to the project's .cursor/mcp.json (or ~/.cursor/mcp.json for all projects):

json
{
  "mcpServers": {
    "sema": {
      "command": "sema",
      "args": ["mcp"]
    }
  }
}

VS Code

bash
code --add-mcp '{"name":"sema","command":"sema","args":["mcp"]}'

Or add it to a project's .vscode/mcp.json — note VS Code uses a servers key:

json
{
  "servers": {
    "sema": {
      "type": "stdio",
      "command": "sema",
      "args": ["mcp"]
    }
  }
}

Zed

Add to Zed's settings.json (also reachable via Settings → AI → MCP Servers):

json
{
  "context_servers": {
    "sema": {
      "command": "sema",
      "args": ["mcp"]
    }
  }
}

OpenCode

Add to the project's opencode.json:

jsonc
// opencode.json — either location works:
//   ./opencode.json                    (project)
//   ~/.config/opencode/opencode.json   (global)
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "sema": {
      "type": "local",
      "command": ["sema", "mcp"],
      "enabled": true
    }
  }
}

For a one-line setup that also wires in the language server, formatter, and theme, use the opencode-sema plugin instead.

Pi

Pi consumes MCP servers through the pi-mcp-adapter extension, which reads the standard MCP config files:

bash
pi install npm:pi-mcp-adapter

Then add Sema to the project's .mcp.json (or the user-global ~/.config/mcp/mcp.json):

json
{
  "mcpServers": {
    "sema": {
      "command": "sema",
      "args": ["mcp"]
    }
  }
}

Hermes

Hermes has no MCP CLI command — add an mcp_servers entry to ~/.hermes/config.yaml:

yaml
mcp_servers:
  sema:
    command: sema
    args: ["mcp"]

Everything Else

Any other client needs the same two facts: command sema, args ["mcp"]. CLI agents mostly follow the mcp add pattern above (e.g. Factory's droid mcp add sema "sema mcp"), and GUI clients — Claude Desktop (claude_desktop_config.json), Cline, Roo Code, Windsurf, JetBrains AI Assistant, Warp, LM Studio — accept the standard mcpServers JSON shown for Cursor.


Default MCP Tools

When started, the MCP server exposes a set of core developer tools:

Tool NameDescriptionParameters
run_fileRun a .sema or .semac file and get output/return valuefile_path (string), arguments (array of strings, optional)
compileCompile a .sema file to .semac bytecodesource_path (string), output_path (string, optional)
evalEvaluate a single Sema expression string and capture outputcode (string)
docsRetrieve docstring and signature details for any symbolsymbol (string)
fmtFormat a Sema file or code stringfile_path (string, optional), code (string, optional)
disasmDisassemble a .sema or .semac file to VM instructionsfile_path (string)
buildCompile a .sema file into a standalone executablesource_path (string), output_path (string)
infoGet environment and version information about the serverNone

Path Resolution

All file paths passed to these tools are resolved relative to the current working directory (CWD) of the MCP server process. Both absolute and relative paths are supported.


Filepath Mode & Custom Tools

You can expose custom tools defined in your Sema scripts to the LLM client by passing filepaths when starting the server:

bash
sema mcp tools/receipts.sema

When started in filepath mode, the server instantiates the interpreter context, evaluates the specified files, and automatically exposes any tools defined via the deftool special form.

Defining a Custom Tool: PDF Receipt Extractor

Here is a real-world example of an MCP tool that reads a receipt PDF and extracts structured data from it by combining Sema's PDF processing and LLM Structured Extraction:

sema
;; tools/receipts.sema

(deftool extract-receipt
  "Extract structured transaction data (merchant, amount, currency, date, line items) from a PDF invoice/receipt."
  {:pdf-path {:type :string :description "Path to the invoice/receipt PDF file (e.g. invoice.pdf)"}}
  (lambda (pdf-path)
    (if (not (file/exists? pdf-path))
        (error (string-append "Receipt file not found: " pdf-path))
        (begin
          (llm/auto-configure)
          ;; Extract text and clean up whitespace for LLM processing
          (define text (text/clean-whitespace (pdf/extract-text pdf-path)))
          ;; Call structured LLM extraction
          (llm/extract
            {:vendor {:type :string :description "Name of the merchant"}
             :amount {:type :number :description "Total bill amount"}
             :currency {:type :string :description "3-letter currency code (e.g. USD, EUR)"}
             :date {:type :string :description "Date of transaction in YYYY-MM-DD format"}
             :line-items {:type :array
                          :description "List of individual items purchased"
                          :items {:type :object
                                  :properties {:description {:type :string}
                                               :price {:type :number}}}}}
            text)))))

Standalone Binary Mode

Sema's build command compiles scripts into standalone native executables. Every compiled standalone binary has built-in MCP server capabilities out-of-the-box:

bash
# Compile the custom receipt tool
sema build tools/receipts.sema -o receipt-extractor

# Run as a normal CLI tool (from your shell/scripts)
./receipt-extractor --pdf-path invoice.pdf

# Start the stdio MCP server exposing the embedded tools
./receipt-extractor --mcp

When started with --mcp, the executable evaluates its embedded bytecode (which registers the extract-receipt tool definition in the environment) and then transitions to starting the stdio MCP server loop.


Tool Filtering & Visibility

When loading files in filepath mode, you can control which tools are exposed to the LLM:

1. Private Prefix

Any tool whose name begins with an underscore (e.g., _secret-helper) is treated as a private helper and excluded from discovery.

2. Declarative Metadata

You can declare a tool as private by adding :mcp/expose #f or :private #t in its parameters metadata map:

sema
(deftool internal-helper
  "Not visible to MCP clients"
  {:mcp/expose #f}
  (lambda () (println "internal")))

3. Command Line Filters

You can explicitly include or exclude tools using the --include and --exclude flags:

bash
# Only expose the receipt extractor tool
sema mcp tools/receipts.sema --include extract-receipt

# Expose all tools except order-pineapple-pizza (which we probably shouldn't be running automatically)
sema mcp tools/receipts.sema --exclude order-pineapple-pizza

4. Sandboxing

By default the server runs unsandboxed — its eval and user-defined tools have full shell/filesystem/network access. Restrict it with --sandbox, using the same grammar as the top-level flag (strict, or a comma-separated capability list):

bash
# Deny shell, network, fs-write, env-write, process, and LLM calls
sema mcp tools/receipts.sema --sandbox strict

# Targeted: everything allowed except shell and filesystem writes
sema mcp tools/receipts.sema --sandbox no-shell,no-fs-write

A denied capability surfaces to the client as a normal tool error (Permission denied: shell requires 'shell' capability). The subcommand flag wins over a top-level --sandbox; with neither, the historical allow-everything behavior is unchanged.


Stateful Notebook Tools

The MCP server exposes a set of stateful notebook management and evaluation tools to allow LLMs to directly read, write, and execute cell-based .sema-nb files.

Tool NameDescriptionParameters
notebook/newCreate a new empty .sema-nb notebookpath (string), title (string, optional), overwrite (boolean, optional — defaults to false; creation fails if a file already exists at path)
notebook/readRead the structure, source, and outputs of a notebookpath (string)
notebook/add_cellAppend or insert a new cell (code or markdown)path (string), type (string: "code"/"markdown"), source (string), after_id (string, optional)
notebook/update_cellUpdate the source/type of an existing cellpath (string), id (string), source (string, optional), type (string, optional)
notebook/delete_cellDelete a cell from a notebookpath (string), id (string)
notebook/eval_cellEvaluate a single code cellpath (string), id (string)
notebook/eval_allEvaluate all code cells in orderpath (string)
notebook/exportExport a notebook to Markdown or a clean .sema scriptpath (string), format (string: "markdown"/"source"), output_path (string, optional)

In-Memory State Caching

To support interactive cell execution (where Cell 2 relies on variables or functions defined in Cell 1), the MCP server maintains an in-memory cache of notebook evaluation engines mapped by their canonical file paths.

When a cell is evaluated, the cached engine runs the code, updates the cell output, saves the updated JSON representation back to disk, and returns the result, ensuring state is preserved across consecutive tool calls.


Sema as an MCP Client

The sections above cover Sema acting as an MCP server. Sema can also be an MCP client — connecting to an external MCP server and consuming its tools from Sema code, so you can use the wider MCP ecosystem (filesystem, GitHub, Slack, databases, hosted vendors like Asana/Linear, …) without hand-writing a deftool for each.

Client builtins

FunctionDescription
mcp/connectConnect to a server (stdio or HTTP), run the initialize handshake, and return an opaque handle
mcp/toolsList the server's tools as descriptor maps ({:name :description :input-schema})
mcp/callCall a tool by name with an arguments map; returns the result (text collapses to a string)
mcp/tools->semaConvert the server's tools into deftool-shaped values ready to hand to defagent
mcp/closeDisconnect (terminate the stdio child / end the HTTP session) and drop the handle

mcp/connect chooses its transport from the config map: :command for a local stdio server, :url for a remote one.

Transports

stdio — launch the server as a child process and exchange JSON-RPC 2.0 over its stdin/stdout. :command is required; :args, :env, and :cwd are optional. A server that needs a credential reads it from the environment Sema hands the child, so pass tokens through :env:

sema
(define fs (mcp/connect {:command "npx"
                         :args ["-y" "@modelcontextprotocol/server-filesystem" "/tmp"]}))

(mcp/tools fs)
; => ({:name "read_file" :description "…" :input-schema {…}} …)

(mcp/call fs "read_file" {:path "/tmp/notes.txt"})
; => "…file contents…"

(mcp/close fs)

;; A server that reads a token from its environment:
(define gh (mcp/connect {:command "github-mcp-server"
                         :env {"GITHUB_TOKEN" (env "GITHUB_TOKEN")}}))

HTTP — connect to a remote server by :url. Sema speaks the modern Streamable HTTP transport (MCP spec 2025-11-25: a single endpoint, Mcp-Session-Id continuity, JSON-or-SSE responses) and auto-detects and falls back to the deprecated 2024-11-05 HTTP+SSE two-endpoint transport when a server only speaks that — so you use the same call either way:

sema
;; Open server, or a static bearer token you already have:
(define gh (mcp/connect {:url "https://mcp.example.com/mcp"
                         :headers {"Authorization" "Bearer ghp_…"}}))

Authenticated remote servers (OAuth)

For a remote server that requires authorization, mcp/connect runs the standards-compliant OAuth 2.1 flow automatically on the first 401 — no bespoke steps:

  1. Discover the authorization server from the WWW-Authenticate challenge → Protected Resource Metadata (RFC 9728) → Authorization Server Metadata (RFC 8414 / OpenID Connect Discovery).
  2. Obtain a client id — a pre-registered one you pass as :auth {:client-id "…"}, a cached one, or Dynamic Client Registration (RFC 7591).
  3. Authorize with the Authorization-Code + PKCE-S256 flow, binding the token to the server with resource= (RFC 8707), by opening your browser and capturing the redirect on a loopback listener (RFC 8252).
  4. Cache & refresh — tokens are stored (OS keychain, or a 0600 file) and refreshed automatically, so later connects are silent.
sema
;; Browser opens on first use; subsequent runs reuse the cached token.
(define asana (mcp/connect {:url "https://mcp.asana.com/mcp"}))

;; Pin a pre-registered client when the server has no dynamic registration:
(define linear (mcp/connect {:url "https://mcp.linear.app/mcp"
                             :auth {:client-id "your-client-id"}}))

Prefer to authenticate ahead of time (recommended, and required on a headless box)? Use the CLI:

bash
sema mcp login  https://mcp.example.com/mcp             # opens a browser
sema mcp login  https://mcp.example.com/mcp --device    # RFC 8628 device-code flow (headless)
sema mcp login  https://mcp.example.com/mcp --client-id ID   # pre-registered client
sema mcp login  https://mcp.example.com/mcp --token T   # install a pre-issued token (CI)
sema mcp logout https://mcp.example.com/mcp             # clear cached credentials
sema mcp list                                           # what's cached, per server

sema mcp list prints one line per server with cached credentials — the canonical URL plus token present or token expired — reading only the local store (no network, and token values are never printed). It can only enumerate the file backend: the OS keychain has no listing API, so list says so and points at SEMA_MCP_TOKEN_STORE=file rather than pretending the store is empty.

Token storage. Credentials are kept per server URL in the OS keychain by default, falling back to a 0600-permission mcp-auth.json in the platform config directory on headless boxes (Linux: $XDG_CONFIG_HOME or ~/.config/sema/; macOS: ~/Library/Application Support/sema/; Windows: %APPDATA%\sema\). Override the backend with an environment variable:

bash
export SEMA_MCP_TOKEN_STORE=file      # force the 0600 file store (no keychain prompts)
export SEMA_MCP_TOKEN_STORE=keychain  # force the OS keychain

Tip (dev): a locally-built (ad-hoc-signed) sema binary gets a new code identity every time you rebuild, so macOS Keychain re-prompts after each cargo build. Set SEMA_MCP_TOKEN_STORE=file while iterating to avoid the prompts.

Using MCP tools in an agent

mcp/tools->sema produces values structurally identical to what deftool yields, so an agent uses them exactly like local tools — no new agent concepts:

sema
(define asana (mcp/connect {:url "https://mcp.asana.com/mcp"}))

(defagent assistant
  {:model "claude-sonnet-5"
   :system "You are an Asana assistant. Use the tools to answer."
   :tools (mcp/tools->sema asana)
   :max-turns 8})

(agent/run assistant "List the tasks assigned to me, with due dates.")

A tool that reports isError surfaces as an error the agent loop feeds back to the model, so the agent can react to failures instead of treating them as success. A full runnable example lives at examples/mcp/asana-tasks.sema.

Security

  • Capabilities. A stdio connection spawns a process (process capability); an HTTP connection is network I/O (network capability). A sandbox that denies the relevant capability cannot open that kind of connection (see the --sandbox flag).
  • Server authority. The tools a server exposes run with the server's authority, not Sema's sandbox — connecting to an untrusted MCP server is equivalent to running untrusted code.
  • Untrusted output. Tool descriptions and results are data, not instructions. They come from the server and can contain prompt-injection (e.g. "tell the user to reconnect to server X"). Treat them as untrusted input; never act on instructions embedded in tool output.

Deterministic testing (cassettes)

MCP tools/call results record and replay through the same cassette tape as LLM calls, so an agent-over-MCP flow can be captured once and replayed offline (no network, no live server) in CI. Record a session, then replay it:

sema
;; Record: real calls run and their results are taped.
(llm/cassette-load "tape.ndjson" {:mode :record})
(define s (mcp/connect {:url "https://mcp.example.com/mcp"}))
(mcp/call s "search" {:q "hello"})
(llm/cassette-save)

;; Replay: the same call is served from the tape without touching the network.
(llm/cassette-load "tape.ndjson" {:mode :replay})

A call is keyed by a hash of the server identity + tool name + arguments; a replay with no matching entry is a hard "miss" so drift is caught. (SEMA_LLM_CASSETTE=tape.ndjson SEMA_LLM_CASSETTE_MODE=replay installs a tape process-wide for a test suite.)

Troubleshooting

  • OAuth login failed / no browser opens — on a headless machine, use sema mcp login <url> --device (device-code flow), sema mcp login <url> --token <token> (pre-issued token), or pass a token directly via :headers {"Authorization" "Bearer …"}.
  • Repeated macOS Keychain prompts while developing — expected for a frequently-rebuilt dev binary; set SEMA_MCP_TOKEN_STORE=file.
  • mcp/connect requires a :command or :url entry — the config map needs one of :command (stdio) or :url (http).
  • Errors surface as SemaError values, so wrap mcp/connect/mcp/call in your usual error handling.