Skip to main content

Raynet MCP

An MCP server that exposes the Raynet CRM REST API to an LLM agent over stdio, built with FastMCP.

The server exposes 32 read tools and 23 write tools covering business cases, companies, people, leads, activities (meetings, tasks, events, phone calls, e-mails), products, price lists, sales orders, offers and invoices, discussion posts and attachments, plus a metadata layer for discovering an instance's value lists and custom fields. Writes are off by default -- see Read and write separation -- so a fresh install is read-only (GET calls only) until RAYNET_WRITE_ENABLED=true is set deliberately.

It was built and validated against a single Raynet instance. The endpoints and tools are generic, but anything this README calls an observation rather than a documented contract -- filter behaviour, value-list contents, which fields an instance renamed -- may differ on yours.

Install

Requires Python >= 3.11.

uv tool install raynet-mcp     # or: pipx install raynet-mcp

Nothing to install if you use uvx raynet-mcp, which fetches and runs it.

The server takes its token from the environment and never reads a .env file itself, so the token belongs in your MCP client's env block.

Claude Desktop, Claude Code (.mcp.json)

{
  "mcpServers": {
    "raynet": {
      "command": "uvx",
      "args": ["raynet-mcp"],
      "env": {
        "RAYNET_API_TOKEN": "your-token-here",
        "RAYNET_WRITE_ENABLED": "false"
      }
    }
  }
}

Codex (~/.codex/config.toml)

[mcp_servers.raynet]
command = "uvx"
args = ["raynet-mcp"]
env = { RAYNET_API_TOKEN = "your-token-here", RAYNET_WRITE_ENABLED = "false" }

With no token in the environment, startup stops with RAYNET_API_TOKEN is not set.

From source

git clone https://github.com/davidesner/raynet-mcp && cd raynet-mcp
uv sync
cp .env.example .env          # then fill in RAYNET_API_TOKEN
uv run --env-file .env python -m raynet_mcp

--env-file is what loads .env into the child process; it applies to a clone only. The installed raynet-mcp console script and python -m raynet_mcp are equivalent entry points.

Configuration

All configuration is read from the environment. This server does not read .env itself: either export the variables, put them in your MCP client's env block, or pass --env-file .env to uv run as shown above. (The test suite is the one exception -- tests/conftest.py loads .env on its own, so uv run pytest -m live works without --env-file.)

Variable Default Meaning
RAYNET_API_TOKEN (required) Bearer token for the Raynet instance. It already carries the instance context, so no separate instance header is needed.
RAYNET_BASE_URL https://app.raynet.cz/api/v2/ API base URL. Regional alternatives exist for other Raynet deployments.
RAYNET_WRITE_ENABLED false Leave unset or false for a read-only server. Setting this to true exposes the 23 write tools.
RAYNET_CACHE_TTL 300 Seconds a value list or custom-field config is cached before being re-read. Business records are never cached.
RAYNET_DATE_FORMAT native Set to iso8601 to request ISO-8601 timestamps from Raynet instead of its native format.
RAYNET_MAX_CONCURRENCY 3 Concurrent requests this client will issue. Capped at 3 regardless of this setting, because Raynet rejects a 5th concurrent connection.
RAYNET_UI_BASE_URL (derived from RAYNET_BASE_URL) Override the host used to build UI deep links, e.g. for a custom domain or reverse proxy.

The ids-first contract

Reference fields on a business case -- phase, type, category, classification 1-3, source -- and similar fields elsewhere are value lists: their ids are specific to this Raynet instance and are not documented anywhere outside it. Never guess or hard-code one.

The intended sequence is:

  1. Call raynet_describe_business_case_fields (for business cases) or raynet_valuelist (for any other list) to read the current ids and labels.

  2. Pass the id you found. For example:

    {"business_case_phase": 123}
    
  3. Optionally, pass the checked pair form instead of a bare id, to have the id and label cross-checked against each other:

    {"business_case_phase": {"id": 123, "label": "Negotiation"}}
    

    This catches a digit slip that a bare id cannot: two adjacent ids in the same list are both "valid" but can mean quite different things.

The same contract applies to writes: every reference id passed to a create or update tool -- phase, type, category, classification 1-3, source, currency, tax rate -- is validated against the instance's own value lists before any request is sent, and a refused write never reaches the network. On success, the result echoes a resolved map from each API field it set to the label of the id it resolved to, so the caller can see in the response which record it actually pointed at, not just the id it supplied.

The tools deliberately do not validate read filter ids (as opposed to ids passed when creating or updating a record, which are validated). This avoids an extra metadata round trip on every list call, and it costs nothing, because Raynet validates filters itself. Observed against a live instance:

mistake what Raynet answers
filter name the endpoint does not have 400 BadRequest: Unsupported filter column [<name>]
reference id that does not exist, on most filters (company, owner, category, …) 404 EntityNotFoundException
reference id that does not exist, on businessCasePhase or businessCaseType 500 ConversionFailedException

So a wrong filter fails loudly rather than silently, and an empty result from a filtered list tool does mean no matching records -- the call would have raised otherwise. The server quotes Raynet's own type and message in the error either way, including on the 500, so the reply says which value it choked on.

These are observations from one live instance, not a documented contract; the status codes may differ elsewhere, but in every case tried the call failed rather than returning an empty page.

owner takes a person id, not a userAccount id. Every tool that accepts owner (creates, updates, and list filters) says so in its docstring, and raynet_users reports the person id to pass. This is not validated by the server: the two id spaces overlap, so a userAccount id that happens to name some other person is accepted -- misattributing a record on a write, or filtering on the wrong person on a read. (An id matching no person at all does fail: Raynet answers 404.) Getting the value from raynet_users is what keeps this straight.

The url field

Every record this server returns (from a _get tool, and from list rows where an id and account are known) carries a url field: a deep link into the Raynet web UI for that exact record, of the form https://<host>/<account>/?view=DetailView&en=<Entity>&ei=<id>. Use it to hand a human a link to the record you just read, rather than describing where to find it.

Some record kinds have no confirmed detail-view route in the web UI (price lists and invoices, at the time of writing); their records simply have no url field. A missing link is intentional in that case -- this server would rather omit a link than hand back one that silently lands on the dashboard.

Read and write separation

This server enforces read-only behaviour in two independent places:

  • The HTTP client's write() method refuses to issue PUT/POST/DELETE unless RAYNET_WRITE_ENABLED=true, raising RaynetWriteDisabled if it is called anyway.
  • At startup, if writes are not enabled, every tool tagged write is deregistered entirely rather than merely disabled. An agent connected to this server never sees a write tool in its tool list unless writes are on -- it cannot discover, let alone call, one.

Tool count

read write total
RAYNET_WRITE_ENABLED unset/false 32 0 32
RAYNET_WRITE_ENABLED=true 32 23 55

(Verified by running the server with list_tools() under both settings; see tests/test_annotations.py.)

Writing to Raynet

Set RAYNET_WRITE_ENABLED=true and restart the server to enable the 23 write tools -- creates, updates, deletes, line-item edits, discussion posts and attachments. With it unset (or false), the write tools are not merely disabled, they are absent from the tool list entirely (see above), and the one HTTP method that could mutate data (client.write) refuses before issuing a request.

A few things that are easy to get wrong:

  • bulk_update does not accept a phase. A phase is only valid for its own business case type, and a bulk selection can span several types, so a single phase id would silently corrupt some of the records it was applied to. Use raynet_business_case_set_phase once per record instead.
  • Line item price excludes VAT; totalAmount includes it. Raynet does not convert between the two, so a value copied from one into the other is wrong by the tax rate.
  • owner is a person id, not a userAccount id -- see The ids-first contract.
  • Every write echoes the resolved label of each reference id it set, in a resolved map on the result, so a wrong-but-valid id is visible in the response rather than silently accepted.
  • raynet_raw_get and raynet_raw_write bypass all validation by design. They exist as an escape hatch for paths this server does not model with a specific tool, and neither one checks a reference id, an entity name, or a field name before sending. raynet_raw_write in particular is the one tool in this server where a wrong path or body does whatever the underlying API permits -- there is no guard between the call and the request, the way there is on every other write tool. Prefer a specific tool where one exists, and treat a raw_write call the way you would treat a raw curl against the API: correct only because you checked it, not because the tool checked it for you.

Testing

Three independent layers of verification exist:

# Offline unit tests. No network access, safe to run anywhere, anytime.
# This is the default: pyproject.toml sets addopts = "-m 'not live'", so a
# bare `uv run pytest` deselects the live suite below and runs 0 live tests.
uv run pytest

# Live read-only tests against a real Raynet instance. Opt-in only -- the
# `-m live` flag overrides the addopts default above. Requires
# RAYNET_API_TOKEN in .env or the environment. Refuses (loudly, via
# pytest.fail) to run at all if RAYNET_WRITE_ENABLED is true, and forces
# writes off for its own session regardless of ambient configuration. Every
# test in this suite issues only GET requests.
uv run pytest -m live -v

# End-to-end check through a real MCP client (the `claude` CLI), driving the
# server exactly as an agent would. Writes are hard-disabled in
# e2e/.mcp.json; this cannot mutate the CRM.
./e2e/run_e2e.sh

A fresh clone with a populated .env never touches the network on a bare uv run pytest or in CI -- the live suite only runs when -m live is passed explicitly, and even then it is read-only.

A note on instance-specific vocabulary

Fields like businessCaseClassification1, ..2 and ..3 are generic slots that each Raynet instance relabels for its own purposes -- one instance's "classification 1" might be called "Industry" and mean nothing like another instance's. The same is true of most value lists. Because of this, the tools in this server deliberately hard-code no instance-specific vocabulary: no list of expected category names, no assumed meaning for a classification slot, nothing that would work on one instance's data and quietly mean the wrong thing on another's. Discover an instance's actual vocabulary with raynet_describe_business_case_fields or raynet_valuelist before assuming anything about what a field means.

Licence

MIT -- see LICENSE.

openapi_raynet.json is the vendor's own API specification, retrieved unmodified from https://app.raynet.cz/api/doc/ and copyright RAYNET s.r.o. It lives in this repository so the contract test in tests/test_openapi_contract.py runs offline, is not included in the published sdist or wheel, and is not covered by this project's licence. See NOTICE. That test skips automatically when the file is absent, so an installed package runs the rest of the suite cleanly.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

raynet_mcp-0.1.0.tar.gz (87.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

raynet_mcp-0.1.0-py3-none-any.whl (50.9 kB view details)

Uploaded Python 3

File details

Details for the file raynet_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: raynet_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 87.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for raynet_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d1af28a0a20c07eb574a24c4047e7bd59a3a2c7a6ae6aaae2ecfaae66f138d50
MD5 e70c6dbc5970cbb21742f88b9b7269e6
BLAKE2b-256 2ece1758a64f8a243a0293f19551f927892b990d852d9861842c1c6f046e312d

See more details on using hashes here.

File details

Details for the file raynet_mcp-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: raynet_mcp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 50.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for raynet_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13f281ec8a8b418b4ac4522f3a6ceea00082771cdeb45644258ec3ba83193374
MD5 3995e1ad59b4383d4c567f282c50829a
BLAKE2b-256 5073ca9a0c2adf3e5fb85640fd97269d6e831965e561f84018fcb594518cb27b

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page