Metadata-Version: 2.5
Name: merofoundry-mcp
Version: 0.1.0
Summary: MCP server exposing the MeroFoundry platform API as agent tools.
Project-URL: Homepage, https://merofoundry.com
Project-URL: Documentation, https://docs.merofoundry.app
License: MIT License
        
        Copyright (c) 2026 Mero Consulting
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: mcp<2,>=1.2.0
Requires-Dist: merofoundry-client<0.2,>=0.1.0
Requires-Dist: pydantic-settings>=2.0
Provides-Extra: dev
Requires-Dist: anthropic>=0.40; extra == 'dev'
Requires-Dist: openai>=1.40; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# merofoundry-mcp

A remote MCP server (Streamable HTTP transport) that exposes 39 task tools over the MeroFoundry
platform API via the shared `merofoundry-client` library. MeroFoundry is the platform for building
entire websites.

## Tools

### Data plane — writes (5 tools)

| Tool | Description |
|------|-------------|
| `describe_schema` | List models and their fields for an application |
| `query_records` | Query records with optional filters and pagination |
| `upsert_record` | Create or update a record (idempotency via `record_id`) |
| `delete_record` | Delete a record by ID |
| `run_rule_event` | Trigger a rule event for an application |

### Authoring — Plan 2 (17 tools)

Authoring tools require an API key with the appropriate authoring permissions (`applications`, `models`, `fields`, `forms`, or `rules` create/update/delete, and `models:publish`).

**Authoring keys carry a larger blast radius than data-plane keys** (full CRUD including delete). Treat them accordingly.

#### Primitives (15)

| Tool | Description |
|------|-------------|
| `create_application` | Create a new application |
| `update_application` | Update an application's metadata |
| `delete_application` | Archive/delete an application (destructive) |
| `define_model` | Create a data model (table). Records require a PUBLISHED model — call `publish_model` after adding fields, or use `scaffold_model` |
| `update_model` | Update a model's metadata |
| `delete_model` | Delete a model and all its records (destructive) |
| `publish_model` | Publish a model's schema so records can be created/queried. Required after creating or changing fields |
| `add_field` | Add a field to a model. Adding a field reverts the model to draft — re-publish before records work |
| `update_field` | Update a field's metadata |
| `delete_field` | Delete a field from a model (destructive) |
| `define_form` | Create a form, optionally bound to a model |
| `update_form` | Update a form's metadata or layout |
| `delete_form` | Delete a form (destructive) |
| `define_rule` | Create a business rule |
| `test_rule` | Dry-run a rule without persisting |

### Reads (8 tools)

Single-item and search reads, plus the file surface.

| Tool | Description |
|------|-------------|
| `get_application` | Fetch one application's metadata |
| `get_model` | Fetch one model and its fields |
| `get_record` | Fetch one record by ID |
| `search_records` | Full-text search across a model's records |
| `upload_file` | Upload a file to an application |
| `get_file` | Fetch a file's metadata and a short-lived download URL |
| `download_file` | Download a file's contents |
| `delete_file` | Delete a file (destructive) |

### Pages (6 tools)

| Tool | Description |
|------|-------------|
| `create_page` | Create a page |
| `list_pages` | List an application's pages |
| `get_page` | Fetch one page, including its layout |
| `update_page` | Update a page's metadata or layout |
| `delete_page` | Delete a page (destructive) |
| `render_page` | Render a page's layout with composites expanded and style resolved |

### Queries, services and governance (3 tools)

| Tool | Description |
|------|-------------|
| `run_saved_query` | Execute a saved query and return its rows |
| `invoke_service` | Invoke a configured outbound service |
| `set_outbound_governance` | Set an application's outbound-call governance policy |

#### Composites (2)

These tools bundle multiple primitive steps into a single call for the common-case paths.

| Tool | Description |
|------|-------------|
| `scaffold_model` | **Fast path:** create a model, add all its fields, and publish it in one call. `fields` is a list of `{name, label?, type, required?, unique?, default_value?, settings?}`. Returns `{model_id, field_ids, published}` |
| `create_form_simple` | **Simple mode:** from a field spec, scaffold a model (create + fields + publish) and create a model-bound form in simple mode — one call. Returns `{model_id, form_id, field_ids}` |

**Publish requirement.** Records require a published model. Creating a model leaves it in draft; `publish_model` promotes it. Adding a field after publish reverts the model to draft, requiring another `publish_model`. The composites handle this automatically — primitives require an explicit `publish_model` call.

**Simple mode is an MCP-layer composite.** `create_form_simple` is implemented entirely in the MCP server; it calls the backend primitives in sequence. The backend does not auto-generate models from forms — `mode` is metadata on the form. There is no magic on the server side.

**Composite partial-failure.** `scaffold_model` and `create_form_simple` are not transactional. If a later step fails (e.g. a field creation error), an earlier-created model may already exist in the backend, possibly in an unpublished state. The caller may need to clean up manually (e.g. `delete_model`) before retrying.

**`create_application` and provisioning.** `create_application` creates an application record, but a newly created application will not have its own API key. Key minting still requires the JWT route; it is out of scope for an app-scoped API key. `provision_tenant` (full control-plane onboarding) is also out of scope.

## Authentication

The caller (MCP client) passes auth on every request as either:
- `Authorization: Bearer <api_key>`
- `X-API-Key: <api_key>`

There is no tenant header. Auth is API-key only.

## Run the server

```bash
MEROFOUNDRY_SERVER_ROOT=http://localhost:8000 \
  uvicorn --factory merofoundry_mcp.server:build_app --port 9000
```

`MEROFOUNDRY_SERVER_ROOT` defaults to `http://localhost:8000` if unset.

## Install / dev

```bash
python -m venv .venv
source .venv/bin/activate
uv pip install -e ../merofoundry-client
uv pip install -e ".[dev]"
```

Run tests:

```bash
.venv/bin/python -m pytest
```

## Eval setup — cross-model matrix

The `evals/` directory contains a harness, Anthropic and OpenAI-compatible adapters, eval tasks, and a matrix runner (`run_evals.py`) that tests tool-use competence across models.

### Prerequisites

**Step 1 — Run meroweb-omo locally.**
The `/api/v1/public` routes must be the hardened version (idempotency, agent-readable errors,
camelCase `operationId`s). This is already on its `main` branch.

**Step 2 — Seed a sandbox application and model.**
Create an application and a model with at least these text fields: `name`, `email`, `company`.

**Step 3 — Mint an API key** scoped to that app with permissions:

For data-plane tasks only:
```
app:read,models:read,records:read,records:create,records:update,records:delete,rules:execute
```

For the `build_app_end_to_end` authoring task (Plan 2), additionally include authoring permissions:
```
applications:create,applications:update,models:create,models:update,models:delete,models:publish,fields:create,fields:update,fields:delete,forms:create,forms:update,forms:delete,rules:create
```

**Step 4 — (For local models) install Ollama and pull models, or start a llama.cpp server:**
```bash
ollama pull llama3.1:8b qwen2.5:7b
```

For llama.cpp, start `llama-server` with tool-call support (the `--jinja` flag enables the model's own chat template to handle tools; required for some models):
```bash
llama-server --jinja -m <model-file> --port 11435
```

**Step 5 — (For the `codex` adapter) start the MCP server and ensure a seeded app + API key.**
The codex adapter drives the live MCP server directly (see [Codex adapter](#codex-adapter--external-agent-deepest-dogfood) below). The server must be running and the API key must be scoped to the seeded app.

**Step 6 — Run the matrix.**

Frontier model (Anthropic):
```bash
python -m evals.run_evals \
  --app <APP_ID> --model <MODEL_ID> --api-key <SK> \
  --adapter anthropic --model-name claude-opus-4-8
```

Local model (Ollama):
```bash
python -m evals.run_evals \
  --app <APP_ID> --model <MODEL_ID> --api-key <SK> \
  --adapter ollama --model-name llama3.1:8b \
  --base-url http://localhost:11434/v1
```

Remote llama.cpp server (OpenAI-compatible endpoint):
```bash
python -m evals.run_evals \
  --app <APP_ID> --model <MODEL_ID> --api-key <SK> \
  --adapter llamacpp --model-name qwen3.5-122b-a10b
  # default --base-url http://192.168.125.220:11435/v1; override with --base-url as needed
```

Codex external agent (drives the live MCP server):
```bash
python -m evals.run_evals \
  --adapter codex \
  --mcp-url http://localhost:9000/mcp \
  --app <APP_ID> --model <MODEL_ID> --api-key <SK>
```

**Step 7 — Read the scorecard.**
The runner prints a per-task, per-model success rate: `success=N/3` (default 3 runs per task).

### Honest scope note

The full cross-model scorecard (tasks creating and querying real records) requires a running meroweb-omo instance with a seeded app and a scoped API key. What is verified today: llama.cpp tool-calling (`qwen3.5-122b-a10b` on the default server emits tool calls correctly) and codex connectivity (v0.140.0 connected and discovered all 5 tools at the time of Plan 1). The full scorecard — including the Plan 2 `build_app_end_to_end` authoring task — requires a live seeded sandbox and an authoring-scoped API key, and has not yet been run.

### llama.cpp adapter

`--adapter llamacpp` treats the llama.cpp server as an OpenAI-compatible endpoint. The default base URL is `http://192.168.125.220:11435/v1`; override with `--base-url`. The model name (`--model-name`) is passed as-is to the API.

If a model/server does not emit tool calls, ensure `llama-server` was started with `--jinja` so the model's own chat template handles tool formatting.

### Codex adapter — external agent (deepest dogfood)

`--adapter codex` is the highest-fidelity eval path: codex acts as the MCP client and drives the live MCP server end-to-end, exercising the full HTTP transport and auth flow. The eval runner injects the MCP server URL into each codex invocation per-run (no mutation of global codex config).

The runner sets `-c sandbox_workspace_write.network_access=true` so codex's sandbox can reach the local MCP server. (Note: the codex config key is `sandbox_workspace_write.network_access`, not `sandbox_permissions`; the latter is rejected by codex.)

**Interactive use (not just evals).** You can also register the server with codex for ad-hoc use:
```bash
codex mcp add merofoundry \
  --url http://localhost:9000/mcp \
  --bearer-token-env-var MEROFOUNDRY_API_KEY
export MEROFOUNDRY_API_KEY=<your MeroFoundry platform API key>
```
codex sends `Authorization: Bearer <key>` on every MCP request. Verified with codex 0.140.0: all 5 data-plane tools (`describe_schema`, `query_records`, `upsert_record`, `delete_record`, `run_rule_event`) are discovered. The 17 authoring tools (Plan 2) follow the same transport and auth flow.

### Release gate — small-model legibility check

> **The small-model success rate is the design-legibility gate.**
>
> If a small local model (e.g. `llama3.1:8b` or `qwen2.5:7b`) cannot complete `create_then_query_contacts`, treat it as a signal to improve the tool descriptions or error messages before shipping — not just as model weakness.

The `create_then_query_contacts` task is the primary gate for data-plane tools: it requires the model to call `describe_schema`, create three records via `upsert_record`, and then query them.

The `build_app_end_to_end` task (added in Plan 2) is the authoring acceptance gate: the model must create a data model with fields, publish it, insert records, and query them — using `scaffold_model` where available. It requires an authoring-scoped API key and a live backend. If small models fail this task consistently, the authoring tool descriptions need work.

## DNS-rebinding host allowlist

FastMCP enables host-header validation by default for localhost-bound servers
(`127.0.0.1`, `localhost`, `::1`).  It accepts only `Host` values matching
`127.0.0.1:*`, `localhost:*`, or `[::1]:*` (port required).

When running behind a reverse proxy or with a real hostname, connections will
receive `421 Misdirected Request` unless you either:
- Configure a `transport_security` allowlist that includes your proxy's
  `Host` header value, or
- Disable the check with
  `TransportSecuritySettings(enable_dns_rebinding_protection=False)`.

For test clients (e.g. Starlette `TestClient`, `httpx`), send
`Host: localhost:80` to satisfy the wildcard check.

For the `codex` eval adapter and interactive codex use, always connect via
`http://localhost:9000/mcp` or `http://127.0.0.1:9000/mcp`. Using a real
hostname or proxy address will produce `421 Invalid Host` until that host is
added to the allowlist.

## Optional live contract check

Verifies the hardened `/api/v1/public` OpenAPI has the expected camelCase `operationId`s. Skipped
when `MERO_OPENAPI_URL` is not set.

```bash
MERO_OPENAPI_URL=http://localhost:8000/api/openapi.json \
  .venv/bin/python -m pytest tests/test_baas_contract.py
```
