Metadata-Version: 2.5
Name: marqov-mcp
Version: 0.3.1
Summary: MCP server for the Marqov quantum orchestration platform
Project-URL: Homepage, https://app.marqov.ai
Project-URL: Source, https://github.com/marqov-dev/marqov-mcp
Project-URL: Issues, https://github.com/marqov-dev/marqov-mcp/issues
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: marqov,mcp,model-context-protocol,qiskit,quantum
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: mcp>=2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# marqov-mcp

An MCP server for the [Marqov](https://app.marqov.ai) quantum orchestration
platform, so Claude Code — or any MCP client, or your own agent — can discover
backends, price runs, execute on a free simulator, and inspect jobs directly.

> **Preview.** Tool contract version `1`. The tools are stable enough to build
> against. Browser authorization and stored credentials are supported (see *Authentication*).

## What it is, and what it is not

This wraps the Marqov **control plane**: backends, cost estimates, jobs, traces,
budget, playground.

It is **not** a Qiskit MCP. A model writes Qiskit competently from its own
weights, and wrapping circuit construction in a tool call spends context to buy
nothing. What a model *cannot* know from its weights is your live fleet, your
prices, your job IDs and your team's budget — which is exactly where tools earn
their place.

## Authentication

You supply your own Marqov API key. The server holds no user model, no session
store and no state; it forwards your key and the platform decides, per request.

Authorise this machine in a browser:

```bash
marqov-mcp login          # shows a code, opens the consent page, stores the key it is granted
marqov-mcp status --check # confirms the platform accepts it
```

You get a short code, a team owner or admin approves it on the Marqov site, and
the key is issued to this machine — scoped, capped and revocable. The approver
can grant *less* than was asked for, so check what `login` reports at the end.

`--level` asks for a permission level (default `simulate`, the lowest that can
run simulations), `--spend-limit` asks for a daily cap in USD, and
`--client-name` sets the name shown on the approval screen.

No browser, or no admin to hand? `marqov-mcp login --paste` still takes a key
you minted yourself.

Or supply it per-process:

```bash
export MARQOV_API_KEY="marqey_..."       # Team -> API keys in the Marqov UI
export MARQOV_BASE_URL="https://app.marqov.ai"   # optional
```

The environment wins if both are present, so an exported key always overrides a
stored one. `marqov-mcp logout` removes the stored credential.

> Only team owners and admins can authorise agent access today. If you are neither,
> ask one of them to approve the code `login` shows you.

Mint the key with the **lowest permission level you need** and a daily spend
limit. The key carries its own permission tier (`browse` / `simulate` / `qpu` /
`seal`), team scope and spend ceiling, all enforced server-side — the server can
do nothing your key cannot.

**Scope is your team, not you personally.** A key is team-scoped, so `list_jobs`
will show work submitted by anyone on the team, not only your own. That is
correct behaviour.

`list_backends` needs no key at all, so you can discover the fleet before you
have an account.

## Install

Requires [`uv`](https://docs.astral.sh/uv/).

```bash
uvx marqov-mcp login
claude mcp add --scope user marqov \
  -e MARQOV_BASE_URL=https://app.marqov.ai \
  -- uvx marqov-mcp
```

Or copy `.mcp.json.example` to `.mcp.json` in a project after logging in. The server
uses the stored credential, or `MARQOV_API_KEY` if explicitly set in its environment.

> Desktop clients may not inherit shell variables. Browser login stores the credential
> where the server can resolve it without putting a literal key into client configuration.

## Tools

| Tool | Needs | What it does |
|---|---|---|
| `list_backends` | — | The fleet: slug, provider, type, qubits, availability |
| `check_backend_availability` | — | Only what is up, with queue times and pricing |
| `estimate_cost` | `browse` | Price a run without running it |
| `run_on_simulator` | `simulate` | Run a program on a free simulator, get counts |
| `list_jobs` | `browse` | This team's jobs, filterable by status and date |
| `get_job_status` | `browse` | Where a job got to |
| `get_job_result` | `browse` | The full result payload |
| `get_job_trace` | `browse` | Execution trace — the debugging tool |
| `get_team_usage` | `browse` | The team's recorded spend for a month, by provider, backend and day |
| `save_script` | `simulate` | Save source to the team's script library; idempotent on a caller-supplied key |
| `get_execution_options` | `browse` | Where a saved script can run, why not elsewhere, and the team's available balance |
| `analyse_script` | `simulate` | The platform's pre-run analysis of a saved script for one backend: cost, checks, `can_run` |
| `submit_managed_run` | `simulate` | Submit a saved task/workflow script as a tracked job on a free managed simulator, under a required cap; dry-run by default |

`get_team_usage` reports spend, not headroom: the usage API does not expose the team's
spending limit or a key's daily cap, and the tool says so rather than inventing a budget.
It also reports `pending_settlement_jobs`, completed paid jobs whose charge has not settled
and so are not yet in the totals.
`get_execution_options` does report the team's available balance, because the platform
returns it with the per-script preflight.

**Preparing a run (milestone 2).** `save_script`, `get_execution_options` and
`analyse_script` take a user to the doorstep of a tracked or paid run with every check the
platform offers. The only submission this server makes is the free managed run described
under milestone 3; paid runs are submitted in the web app or the SDK. `analyse_script` returns the
`analysis_id` a paid submission requires, which is single-use and expires after fifteen
minutes; the user submits it with the `script_id` in the web app or the SDK. Analysing a
saved script persists an analysis row on the platform, which is why it needs `simulate`.
`save_script` sends one request with an `Idempotency-Key` (pass `idempotency_key` to make
a retry identical) and returns the key with every outcome.

**Submitting a tracked run (milestone 3).** `submit_managed_run` is the one tool that can
create a job. It speaks the platform's managed contract (`marqov.public-submission/v1`),
which admits a run atomically against a mandatory per-job cap, and it is restricted on
purpose:

- **Free managed simulators only.** Today that is `marqov-sim`, the one managed destination
  the platform also vouches as free of provider cost. Paid hardware stays unreachable from
  this server until an API key can cancel a job.
- **A cap is required and bounded twice.** The caller must pass `cap_cents`, the most the
  platform may reserve of the team's funding for the job. The platform caps a job at $100;
  this server refuses anything above 500 cents by default. An operator may raise that with
  `MARQOV_MCP_MAX_CAP_CENTS`, never past 2500. "Free of provider cost" does not mean the
  platform reserves nothing: the cap covers compilation and tasks, and an insufficient
  balance or spending limit is refused with HTTP 402.
- **Dry run by default.** The default call checks the key's team, the runtimes enabled for
  it, the cap against the runtime's range, and the saved source, then returns the exact
  request without sending it. `dry_run=false` sends it. `MARQOV_MCP_SUBMIT_DRY_RUN=1` forces
  dry run whatever the caller passes.
- **One key, identical bytes.** The platform deduplicates on the UUID `Idempotency-Key` plus
  the body and returns the original receipt for an identical retry, so the client's bounded
  retry of a connection error or a 503 is a replay, not a second job. A read timeout is
  never retried. Every outcome returns the key, and the advice is always to retry with the
  same key, never a fresh one.
- **Source is pinned.** The submission carries the SHA-256 of the saved script as read during
  preflight; if the script changes before admission the platform refuses with HTTP 409, and
  a receipt describing different source is reported as an error that names the admitted job.

Admission means funded and queued, not finished: follow the returned `job_id` with
`get_job_status`.

### Projects tools (opt-in, default off)

Thirteen more tools read and write a team's [Projects](https://app.marqov.ai) (living lab
notebooks) and can seal one into an immutable Capsule. They are **not registered unless
you set `MARQOV_MCP_ENABLE_PROJECTS=1`**, because they are a step past this server's
read-and-simulate posture and whether they stay is an open decision. With the flag off
the surface is exactly the thirteen tools above.

| Tool | Needs | What it does |
|---|---|---|
| `list_projects` | `browse` | The team's Projects |
| `read_project_workspace` | `browse` | The Workspace's blocks, paged and trimmed; one block in full by id |
| `list_scripts` | `browse` | Discover saved team scripts with pagination |
| `read_script` | `browse` | Read current source and requirements in pages, with content digests |
| `create_project_from_job` | `simulate` | Seed a new Project from an existing completed job; dry-run by default |
| `add_markdown_block` | `simulate` | Append a markdown block |
| `add_script_block` | `simulate` | Append a block referencing a saved script, optionally associated with the completed job that ran it |
| `add_result_block` | `simulate` | Append a result block pinned to a completed job |
| `add_image_block` | `simulate` | Upload a local PNG/JPEG/GIF/WebP and append it |
| `add_dated_update` | `simulate` | Append a dated update, optionally with a first note |
| `get_seal_readiness` | `browse` | Blockers and warnings a Capsule seal would hit; read-only |
| `prepare_capsule` | `browse` | Check readiness, metadata and version conflicts; return a proposed seal request |
| `seal_project` | `seal` | Seal into an immutable Capsule (dry-run by default, needs `certify=true`) |

A Project can have several Workspaces. These tools use the one the platform names as
primary, never an archived one, and refuse rather than guess if several are active and none
is primary. The platform seals one Workspace, so readiness blocks while several are active.

Design rules: writes are **append-only** (no edit or delete of an existing block) and
target the **Workspace only**, never the Report. Block content is user-authored data
and is described to the model as such.

Update notes are new blocks; even the empty markdown block seeded by the platform
is left untouched. Projects writes are sent once. Ordinary appends and seals do not
deduplicate retries. After an ambiguous failure, inspect the Workspace or Capsule versions before
retrying. If a dated update succeeds but its note fails, the error includes the new
`update_group_id` so the caller can append the note without recreating the update.

The from-job creation route deduplicates on the `Idempotency-Key` plus the request body.
Its two replay outcomes are named: a key still being processed (HTTP 409) says to wait
and retry with the same key and identical arguments; a key reused with different
arguments (HTTP 422) says to inspect existing state before choosing a new key. Other
conflicts (HTTP 409, such as an existing Capsule version) and rejected writes (HTTP 400)
state that nothing new was created.

All failed tool operations return the standard MCP `isError: true` alongside an
actionable text explanation, including missing credentials, permission failures,
HTTP errors and transport failures. Readiness checks fail when a dependency cannot
be checked, so unavailable data cannot produce a false readiness result.

Tools also advertise MCP read-only, destructive and idempotency hints. These describe
effects for downstream clients; platform authorization remains the enforcement point.

**Bounded reads and pagination.** `list_projects` returns `next_offset`; pass it as
`offset` to continue until null. `read_project_workspace` pages blocks with `next_offset`.
To read a long block, supply `block_id`, then pass its `next_content_offset` as
`content_offset` until null. `max_chars` for workspace previews is limited to 0–4000.
Each page of a block carries `content_sha256` for the whole block; if it changes between
pages, a colleague edited the block mid-read and the pages must not be joined.

`get_job_result` keeps small results as their original JSON. Large results return a
`json-text-page` object with `content`, `total_chars`, `content_sha256` and `next_offset`.
Concatenate the content pages in order and parse the combined JSON; pass `next_offset` as
`offset` until null. `max_chars` is 1–4000. Offsets count characters, including Unicode.
No available result is an explicit error; inspect `get_job_status` for the job state.

Every tool response is redacted and capped at 32,000 characters. Oversized responses
without dedicated pagination return an explicit truncated preview. Invalid JSON and
platform error envelopes never become successful tool results.

**Dry run.** Every write takes `dry_run`. A dry run does the reads it needs, records the
exact request it would have sent, and sends nothing. `MARQOV_MCP_PROJECTS_DRY_RUN=1`
forces this for every write whatever the model passes; `MARQOV_MCP_DRY_RUN_LOG=/path.jsonl`
appends each record as a JSON line. `seal_project` defaults to dry run, and a real seal
needs `dry_run=false` **and** `certify=true`, because the certification is a statement
about accuracy that belongs to the user.

The in-memory dry-run history retains the latest 100 records. The optional JSONL log
is the durable record.

`get_seal_readiness` checks the platform's structural requirements and additionally
blocks empty source, unavailable result payloads, empty narrative and placeholder-only
narrative. The seal endpoint remains the authority. As on the platform, having no
result blocks at all is a warning, permitting reference-only Capsules.

Readiness also returns `evidence`: one entry per script and result block saying what it
pins and whether a seal would carry a submitted-source snapshot (`executed_source_sealed`)
or the current saved revision. The list is bounded to 60 entries, with `evidence_truncated`
set when a Project has more. `capsule_source_policy` states the platform's rule in words,
and `workspace_fingerprint` digests the blocks a seal would read.

### Existing results to Capsule preparation

1. Find a completed run with `list_jobs`, then inspect `get_job_status` and
   `get_job_result`. Keep the exact job ID with the observations; a free synchronous
   simulator run has no job record and cannot become a result-block reference.
2. Choose an existing Project with `list_projects`, or preview `create_project_from_job`.
   To create the new notebook, repeat with `dry_run=false` and the preview's
   `idempotency_key`. This route supports idempotent recovery: retain the key and
   identical arguments after an uncertain response. The tool verifies that the new
   Workspace actually contains the expected result block; partial failures preserve
   the project ID so the caller can recover without creating another notebook.
3. Read the Workspace. A notebook created from a job returns `result_block_ids` and
   any `script_ids`. Inspect saved code and dependencies with `read_script`; use
   `list_scripts` to discover existing library references. Follow `next_offset`
   and check `content_sha256` stays constant across script pages.
4. For an existing Project, append the intended result with `add_result_block` and,
   when verified, the saved-script reference with `add_script_block`. Result appends
   check that the job completed and has a payload; script appends reject empty or
   deleted source. A library match alone does not establish an execution relationship.
   When the job ran that exact saved script, pass its `job_run_id` to `add_script_block`
   so the platform records the association and seals the submitted-source snapshot. The
   tool reads the association back from the receipt: a platform that does not record it
   returns an error naming the appended block as reference-only, so the caller never
   describes current source as executed code and never appends the block twice.
5. Append narrative with `add_markdown_block` or `add_dated_update`, citing exact job
   and result-block IDs. Distinguish observations, interpretation, and provenance
   limitations. Seeded narrative placeholders are preserved; readiness warns that they
   will appear in the Capsule and blocks preparation until substantive narrative exists.
6. Call `prepare_capsule` with the intended name, new version, and contributor details.
   It uses read-only requests, checks metadata and existing versions, and returns
   `prepared_for_seal`, blockers, warnings, and the proposed request. It never records
   certification or seals. `seal_project` with its default dry-run can record that
   same request. An ordinary dry run records intent; it does not validate readiness.
7. Preparation and sealing are tied together by `workspace_fingerprint`. Pass it to
   `seal_project` as `expected_workspace_fingerprint`: the seal re-reads the Workspace
   and refuses, sending nothing, if any block was added, reordered or edited since the
   user reviewed the prepared request. A dry run with the fingerprint reports
   `workspace_fingerprint_matches` without sealing.

**Provenance limit:** the platform snapshots the submitted source and its SHA-256 on
every job at submission, and its seal route uses that snapshot for a script block that
is associated with the job. But the public job-status API exposes neither the snapshot,
its hash nor `script_id`, so this server cannot verify an association itself; it reports
what the platform recorded. A script block with no association is sealed from the
current saved revision, which can change after a run. `read_script` explicitly returns
the current saved revision, and reference-only script blocks must not be described as
verified execution snapshots. An inline-code job may have no saved script; do not attach
an unrelated library entry to bypass a readiness blocker. Recording an association from
`add_script_block` or `create_project_from_job` needs a platform that accepts
`job_run_id` on script blocks (marqov-platform PR #2750, unmerged at the time of writing);
on the current platform both tools report the reference as unassociated.
Actual sealing still requires explicit user instruction and certification plus a
seal-tier key; preparation is an advisory check, not a guarantee of platform acceptance.

**Submitting to real hardware is not available through this server.** It can show
you QPUs and price them, but not run on them — use the web UI or the `marqov` SDK.

## Develop

```bash
uv run --extra dev pytest          # offline suite
uv run --extra dev ruff check .
MARQOV_API_KEY=marqey_... uv run --extra dev pytest -m live   # real platform
```

The live suite is opt-in and hits `app.marqov.ai`. It uses `MARQOV_API_KEY` if set,
otherwise the credential stored by `marqov-mcp login`. It is worth running before any
release: both bugs found in this server's first live run — a playground cold start
outrunning the client timeout, and a 404 blaming team scoping for a job that simply
had no trace — were invisible to the mocked tests. Note that it includes one free
simulator execution (`run_on_simulator`), which creates no job record and costs nothing.

### Releasing

Releases are published to PyPI by `.github/workflows/publish.yml` through PyPI trusted
publishing, so no token lives in the repository. To cut one:

1. Set `version` in `pyproject.toml` and `__version__` in `src/marqov_mcp/__init__.py`,
   move the `CHANGELOG.md` heading from "unreleased" to the date, and merge.
2. Create a GitHub release tagged `vX.Y.Z` (the workflow refuses a tag that does not
   match the package version).
3. The workflow runs Ruff and the offline suite, builds, and publishes. `uvx marqov-mcp`
   then resolves for everyone.

One-time setup is described at the top of the workflow file: a pending trusted publisher
on pypi.org for `marqov-mcp` pointing at this repository and workflow, and a GitHub
environment named `pypi`.

## Status

Preview — see `marqov-dev/marqov-platform` epic #1405. The default surface covers watching,
preparing, and free managed runs; see `CHANGELOG.md`.

## Licence

Apache-2.0.
