Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

MCP server

Elyra Sjá can run as an MCP server, so your own AI assistant — Claude Desktop, Zed, Cursor, or anything else that speaks MCP — can call the analysis engine while you work.

This inverts the app's usual direction. Normally you drive Sjá and it can call a model for you (Run with AI). Here a model drives Sjá: you ask your assistant to look at a page, and it comes back with the same evidence the app would show you.

elyra-sja mcp

The server speaks JSON-RPC 2.0 over stdio. It is read-only and stateless: no file writes, no AI keys spent, no database, nothing written to disk.


Contents


Setting it up

The server is the same binary as the app, so there is nothing extra to install. Its path inside the bundle is:

/Applications/Elyra Sjá.app/Contents/MacOS/elyra-sja

Note the non-ASCII á and the space. Most clients handle it, but if yours struggles, symlink it somewhere plain:

sudo ln -sf "/Applications/Elyra Sjá.app/Contents/MacOS/elyra-sja" /usr/local/bin/elyra-sja

and use /usr/local/bin/elyra-sja in the configuration below.

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "elyra-sja": {
      "command": "/Applications/Elyra Sjá.app/Contents/MacOS/elyra-sja",
      "args": ["mcp"],
      "env": {
        "ELYRA_SJA_LICENSE": "SJA-…"
      }
    }
  }
}

Restart Claude Desktop. The tools appear under the plug icon in the composer.

Zed

In ~/.config/zed/settings.json:

{
  "context_servers": {
    "elyra-sja": {
      "command": {
        "path": "/usr/local/bin/elyra-sja",
        "args": ["mcp"],
        "env": { "ELYRA_SJA_LICENSE": "SJA-…" }
      }
    }
  }
}

Cursor

In ~/.cursor/mcp.json (or .cursor/mcp.json in a project, to scope it):

{
  "mcpServers": {
    "elyra-sja": {
      "command": "/usr/local/bin/elyra-sja",
      "args": ["mcp"],
      "env": { "ELYRA_SJA_LICENSE": "SJA-…" }
    }
  }
}

Any other client

Anything that launches a command and speaks MCP over stdio will work. The command is elyra-sja mcp; there are no flags, no ports and no config file.

To check the server by hand, without a client:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | elyra-sja mcp

You should get one line of JSON on stdout and a ready on stdio line on stderr.


Licence

The MCP server needs a valid licence, like headless mode. It looks in two places, in order:

  1. ELYRA_SJA_LICENSE — set it in the client's env block, as above.
  2. The Keychain — if the app is activated on this machine, the stored code is used automatically and you can leave env out entirely.

A missing licence does not stop the server from starting. The client connects, the tools are listed, and the first call returns an explanation:

Elyra Sjá's MCP server needs a licence. Set ELYRA_SJA_LICENSE to a signed code,
or activate the app on this machine so the code is in the Keychain.

That is deliberate: exiting at startup shows up in an assistant's UI as an unexplained crash, with nothing to diagnose.


Tools

analyze_page

Analyze one page for SEO and GEO readiness. Fetches the page and nothing else.

Parameter Type Required Notes
url string yes Page to analyze
keyword string no Enables keyword-placement checks
recipe string no audit (default), optimize, indexed, monitor, ai_visibility, research — biases the scoring
locale string no BCP 47 market locale (e.g. de-DE), sent as Accept-Language. Omit to analyze the page exactly as served

robots_check

Whether a specific crawler may fetch a URL, per the site's robots.txt. Resolves the most specific user-agent group, so a site that allows * and disallows GPTBot is correctly reported as blocking it.

Parameter Type Required Notes
url string yes URL to test (the path matters — rules are per path)
agent string no Crawler token; default GPTBot. E.g. OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Googlebot, CCBot, Bytespider

The answer also says what that crawler is for, because the cost of blocking it depends entirely on that: an answers crawler blocked means absence from AI answers today, while a training crawler blocked costs no visibility at all.


Worked examples

Asking for an audit

You: Have a look at https://acme.test/pricing — I care about AI visibility, the keyword is "widgets".

The assistant calls analyze_page with {"url": "https://acme.test/pricing", "keyword": "widgets", "recipe": "ai_visibility"} and receives:

https://acme.test/pricing — score 74/100 (C)
HTTP 200 · 12 pass / 5 warn / 2 fail

Categories:
  meta           100
  structure      71
  social         50
  geo            56
  indexing       78

Needs attention:
  [FAIL] Content depth (structure) — 41 words — too thin to rank or be cited.
  [WARN] OpenGraph tags (social) — Missing: og:description, og:image.
  [WARN] Twitter Card (social) — No twitter:card meta tag.
  [FAIL] Structured data (JSON-LD) (geo) — No JSON-LD — AI engines can't parse entities reliably.
  [WARN] Citable schema type (geo) — Add FAQPage / Article / HowTo schema for AI citations.
  [WARN] Canonical URL (indexing) — Canonical points elsewhere: http://127.0.0.1/pricing
  [WARN] Internal linking (indexing) — Only 2 internal links — add contextual links.

2 question-style headings with answers on the page.

Because the output names the category and the evidence, the assistant can act on it: write the missing OpenGraph tags, or draft FAQ schema from the two question headings the page already has.

Checking a crawler

You: Can GPTBot read that page?

robots_check with {"url": "https://acme.test/pricing", "agent": "GPTBot"}:

GPTBot is NOT allowed to fetch https://acme.test/pricing
Governed by the group for: gptbot
  1 disallow rule(s), 0 allow rule(s)

GPTBot is operated by OpenAI and is used for training future models — blocking it
costs no visibility today.

And the crawler that actually matters for answers:

OAI-SearchBot is allowed to fetch https://acme.test/pricing
Governed by the group for: *
  1 disallow rule(s), 0 allow rule(s)
  Crawl-delay: 2.0s

OAI-SearchBot is operated by OpenAI and is used for answering user questions now —
blocking it means absence from those answers.

Note how the two answers come from different groups in the same file. That distinction is the whole point: a tool that only reads User-agent: * would have called GPTBot allowed.

Prompts that work well

  • "Audit these five URLs and tell me which one to fix first, and why."
  • "Is this page blocked for any of the AI crawlers that answer questions?"
  • "Compare the German and English versions of this page — use locale de-DE for one."
  • "This page scores badly on GEO. Look at it, then rewrite the headings so the answers are self-contained."

The last one is where this gets genuinely useful: the assistant has the finding and your file open.


Protocol details

  • Revision: 2024-11-05
  • Transport: stdio, one JSON object per line
  • Methods: initialize, tools/list, tools/call, ping
  • Notifications (no id) receive no response, per the JSON-RPC spec

initialize response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "elyra-sja", "version": "0.5.0" },
    "instructions": "Read-only SEO/GEO analysis. analyze_page scores a page and lists what fails; robots_check says whether a given AI crawler is allowed to fetch a URL."
  }
}

Error semantics

A tool failure — a page that won't load, a missing licence, a rate limit — comes back as a successful RPC with isError:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "Could not analyze https://…: Request failed: …" }],
    "isError": true
  }
}

The model then sees the reason and adapts. A JSON-RPC error object is reserved for actual protocol faults, so an assistant doesn't report "the server broke" when a page simply 404s:

Code Meaning
-32700 Invalid JSON on the wire
-32600 Missing or wrong jsonrpc version
-32601 Unknown method
-32602 Missing params, or an unknown tool name

Limits and guarantees

Read-only, by design. Only analyze_page and robots_check are exposed. The file tools that Run with AI uses are not reachable through MCP, and neither is anything that spends an AI API key or touches the database. An assistant in a loop must not be able to edit your checkout through a side door. A test pins the tool list to exactly those two names, so adding a third has to be a deliberate act.

Rate limited to 30 calls per rolling minute. Sjá fetches real pages from real sites; an assistant will happily iterate, and the caller has no limiter of its own. Beyond the limit you get an isError result explaining the wait.

Nothing on disk. No database is opened, no app data directory is created, no temporary files are left. A test asserts this by pointing HOME at an empty directory and checking it stays empty.

stdout is the protocol. Every log line, warning and diagnostic goes to stderr. One stray line on stdout would corrupt the stream — which is why the server prints nothing there but responses.

Requests are not cached. Two identical calls fetch the page twice. That is honest — a page can change between calls — but worth knowing if you ask an assistant to iterate on the same URL.


Troubleshooting

The server doesn't appear in the client. Run the command by hand first:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | \
  "/Applications/Elyra Sjá.app/Contents/MacOS/elyra-sja" mcp

A JSON line on stdout means the server is fine and the problem is in the client's configuration — usually the path. Try the symlink from Setting it up.

Every tool call says it needs a licence. Either ELYRA_SJA_LICENSE isn't reaching the process (client env blocks are not your shell environment — a login shell's exports are usually not inherited), or the code has expired. Codes from before 0.3.0 (ELYRA-…) no longer validate; a current one looks like SJA-….

Calls fail after a burst. That's the rate limit. Wait a minute, and ask the assistant to work through URLs in smaller batches.

The assistant says the server crashed. Check the client's MCP log — the server writes diagnostics to stderr, which clients capture. A genuine crash is a bug: please report it with that log at elyracode.com/sja.

Analysis is slower than in the app. The app can reuse a warm HTTP connection pool across a session; a freshly launched server pays for the first TLS handshake. Subsequent calls in the same session are faster.


See also