mcp-server-raynet

MCP server for the Raynet CRM

console application
Instalace
apt install mcp-server-raynet
Popis

An MCP (Model Context Protocol) server exposing the Raynet CRM REST API as tools for an LLM agent, built on fastmcp. It runs over stdio and is registered into a local mcprack catalog by its companion package.

Tools cover companies, persons, leads, business cases with line items, activities, products, price lists, offers, sales orders, invoices, discussion posts and attachments, plus a metadata layer for discovering an instance's value lists and custom fields. The API token is read from the environment and is never a tool parameter.

The server is read-only until RAYNET_WRITE_ENABLED=true is set deliberately; write tools are then registered, with deletes marked as destructive in their MCP annotations.

🌐 Domovská stránka📋 Detail balíčku

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 27 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.

Debian / Ubuntu (VitexSoftware repository)

apt install mcp-server-raynet

That installs /usr/bin/mcp-server-raynet (the old raynet-mcp name stays working as a compatibility symlink). Installing mcprack-mcp-server-raynet additionally self-registers the server into a local mcprack catalog, read-only by default (RAYNET_WRITE_ENABLED unset). The manifest mcprack reads to pre-fill the env form ships as /usr/share/mcp-server-raynet/server.json.

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 27 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 27 59

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

Tool titles and annotation classes

Every tool declares its title and all four MCP annotation hints. The class column summarises the hints: read = read_only, create = not idempotent, update = idempotent but not destructive, delete = destructive. Write tools appear only when RAYNET_WRITE_ENABLED=true.

Tool Title Class
raynet_activity_complete Complete Activity update
raynet_activity_delete Delete Activity delete
raynet_activity_get Get Activity read
raynet_activity_list List Activities read
raynet_attachment_link Link Attachment create
raynet_attachment_upload Upload Attachment create
raynet_business_case_bulk_update Bulk Update Business Cases update
raynet_business_case_create Create Business Case create
raynet_business_case_create_with_items Create Business Case With Items create
raynet_business_case_delete Delete Business Case delete
raynet_business_case_get Get Business Case read
raynet_business_case_item_add Add Business Case Item create
raynet_business_case_item_delete Delete Business Case Item delete
raynet_business_case_item_update Update Business Case Item update
raynet_business_case_items List Business Case Items read
raynet_business_case_list List Business Cases read
raynet_business_case_participants List Business Case Participants read
raynet_business_case_phase_changes List Business Case Phase Changes read
raynet_business_case_set_phase Set Business Case Phase update
raynet_business_case_update Update Business Case update
raynet_company_create Create Company create
raynet_company_delete Delete Company delete
raynet_company_get Get Company read
raynet_company_list List Companies read
raynet_company_price_lists List Company Price Lists read
raynet_company_relationships List Company Relationships read
raynet_company_update Update Company update
raynet_custom_fields List Custom Fields read
raynet_describe_business_case_fields Describe Business Case Fields read
raynet_discussion_post Post Discussion Message create
raynet_discussion_posts List Discussion Posts read
raynet_invoice_get Get Invoice read
raynet_invoice_list List Invoices read
raynet_lead_create Create Lead create
raynet_lead_delete Delete Lead delete
raynet_lead_get Get Lead read
raynet_lead_list List Leads read
raynet_lead_update Update Lead update
raynet_meeting_create Create Meeting create
raynet_meeting_update Update Meeting update
raynet_offer_get Get Offer read
raynet_offer_list List Offers read
raynet_person_create Create Person create
raynet_person_delete Delete Person delete
raynet_person_get Get Person read
raynet_person_list List Persons read
raynet_person_update Update Person update
raynet_price_list_items List Price List Items read
raynet_price_list_list List Price Lists read
raynet_product_get Get Product read
raynet_product_list List Products read
raynet_refresh_metadata Refresh Metadata Cache read
raynet_sales_order_get Get Sales Order read
raynet_sales_order_list List Sales Orders read
raynet_task_create Create Task create
raynet_task_update Update Task update
raynet_users List Users read
raynet_valuelist Read Value List read
raynet_whoami Who Am I read

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.
  • There is no raw escape hatch. Earlier versions exposed raynet_raw_get and raynet_raw_write, which took a caller-supplied path and body and checked neither. They were withdrawn in 0.2.0: an agent-steerable path and an unvalidated body are too sharp an edge to hand a model, and a CRM record carrying injected instructions could aim them at any endpoint the token can reach. Every write now goes through a tool that knows its own endpoint. If you need an endpoint this server does not model, open an issue -- or call the API directly, where the decision is yours.
  • A write echo reports what was sent, not what the server kept. Raynet derives some fields rather than storing what you give it -- a business case's totalAmount is recomputed from its line items once any exist, so a total passed to raynet_business_case_update is discarded while the echo still shows it. Read the record back when a derived field matters. Raynet also stores descriptions as HTML, so typographic quotes come back as &quot;; this server neither escapes nor unescapes, so what you see is what is stored.
  • Six tools delete a whole record -- a client, person, lead, business case, business-case line item, or one activity. Each is annotated destructiveHint, so a client that filters on annotations can surface or withhold them. There is no cascade and no force: Raynet refuses while other records still reference the target, and that refusal is passed through unchanged. raynet_activity_delete requires type because the five activity kinds have separate id spaces -- the same number is a different record in each.
  • state, role and rating on a client, and priority on a lead, are required by the API and this server hard-codes no accepted values. They are string codes rather than value lists, and an instance can use a code the vendor document does not name -- live data carries role="E_OWN", which the document's enum omits. Read the codes your instance uses off existing records (raynet_company_list and raynet_lead_list return them) rather than guessing, and expect the API, not this server, to reject a bad one.

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.

One suite needs a one-off fetch. tests/test_openapi_contract.py -- 197 of the tests, which checks every path and write body against the vendor's OpenAPI document -- skips itself unless that document is on disk, because this repository does not redistribute it:

# Retrieves the vendor document into openapi_raynet.json (git-ignored).
# Stdlib only, so it runs before `uv sync`. Needed once per clone; the
# offline suite stays offline afterwards.
python3 scripts/fetch_spec.py

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.

The RAYNET CRM API specification this server is built against is copyright RAYNET s.r.o. and is published by the vendor at https://app.raynet.cz/api/doc/. This repository carries no copy of it, and it is not covered by this project's licence -- run python3 scripts/fetch_spec.py to fetch it locally when you want the contract test. The version the tools and that test's tables were written against is RAYNET CRM API 2.0.0 (OpenAPI 3.0.3, 276 paths), retrieved 2026-08-25. See NOTICE.

Balíček „mcp-server-raynet“ nebyl nalezen

Tento balíček zatím možná není v naší databázi zaindexován. Zkuste to prosím později, nebo zkontrolujte přímo repozitář balíčků.

Procházet repozitář