Metadata-Version: 2.5
Name: sodir-mcp
Version: 0.1.0
Summary: Read-only MCP server for Sodir (Norwegian Offshore Directorate) FactPages: wellbore facts and monthly field production, from a local cache you fetch yourself.
Author: Joyce Ferreira
License-Expression: MIT
License-File: LICENSE
License-File: NOTICE-DATA.md
Keywords: factpages,mcp,model-context-protocol,norway,oil-and-gas,sodir,volve
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.12
Requires-Dist: httpx>=0.28
Requires-Dist: mcp<3,>=2.3
Requires-Dist: pydantic>=2
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.30; extra == 'otel'
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.2; extra == 'postgres'
Description-Content-Type: text/markdown

# sodir-mcp

A read-only [MCP](https://modelcontextprotocol.io) server for the public Sodir (Norwegian Offshore
Directorate) FactPages. It answers two questions with sourced, unit-labelled data:

| Tool | What it returns |
| --- | --- |
| `wellbore_facts` | Official facts of one wellbore: status, purpose, content, operator, field, spud (entered) and completion dates, total depth (MD), Sodir final vertical depth (TVD), kelly bushing elevation, water depth, drilling facility, surface location. |
| `field_production_monthly` | Sodir's monthly saleable production of a whole field (oil, gas, NGL, condensate, oil equivalents, produced water) with totals over a window. |

The package contains **no Sodir data**. You fetch the public tables once into a local cache, and the
tools read that cache (read-only). Nothing is sent anywhere else.

> **Data licence:** the Sodir data is published under the
> [Norwegian Licence for Open Government Data (NLOD) 2.0](https://data.norge.no/nlod/en/2.0). Every
> tool response carries the NLOD attribution; keep it if you pass the data on. See
> [NOTICE-DATA.md](NOTICE-DATA.md). The code of this package is MIT-licensed.

## Install and first run

```bash
uvx sodir-mcp refresh          # downloads about 8 MB of CSV from factpages.sodir.no into the cache
uvx sodir-mcp status           # what the cache holds, and when it was fetched
uvx sodir-mcp                  # serve over stdio (same as `sodir-mcp serve`)
```

(`uvx` runs the published package. Before it is published, install from a built wheel:
`pip install dist/sodir_mcp-0.1.0-py3-none-any.whl`.)

Claude Desktop (`claude_desktop_config.json`):

```json
{"mcpServers": {"sodir": {"command": "uvx", "args": ["sodir-mcp"]}}}
```

Until `refresh` has run, each tool answers `data_not_loaded` with the hint `run: sodir-mcp refresh`.
Set `SODIR_MCP_AUTO_REFRESH=1` to let the first tool call download the data (off by default, so
nothing downloads without being asked).

## Commands

| Command | Does |
| --- | --- |
| `sodir-mcp refresh [--force] [--from-dir DIR]` | Downloads `field`, `wellbore_development_all`, `wellbore_exploration_all` and `field_production_monthly`, checks that each is CSV with the expected columns, normalises, and replaces the cache atomically. A failed refresh keeps the previous cache. At most once per 24 hours unless `--force`. `--from-dir` reads the four `<table>.csv` files from a folder you downloaded yourself (offline path; only this command reads files). |
| `sodir-mcp status` | Shows the cache path, the tables, fetch time and SHA-256. |
| `sodir-mcp serve [--http --host H --port P]` | MCP over stdio (default) or streamable HTTP. |

Environment: `SODIR_MCP_CACHE` (cache folder; default is the platform cache folder, for example
`~/Library/Caches/sodir-mcp` on macOS or `~/.cache/sodir-mcp` on Linux), `SODIR_MCP_AUTO_REFRESH=1`,
`SODIR_MCP_BACKEND=postgres` (see below). The only host the package contacts is
`factpages.sodir.no`; redirects to other hosts are refused. A cache older than 90 days adds a note
to every answer.

## Example

`wellbore_facts(wellbore="F-12")` (a Volve alias; any exact Sodir name such as `15/9-F-12` works,
for other fields only exact Sodir names). Trimmed output with values from the fixture used in the
tests (hand-made, not a Sodir extract):

```json
{"wellbore": "15/9-F-12", "npd_wellbore_id": 5599, "category": "development",
 "status": "P&A", "purpose": "PRODUCTION", "spud_date": "2007-06-14",
 "completion_date": "2007-08-27", "total_depth_md_m": 3520.0,
 "final_vertical_depth_tvd_m": 3107.0, "kb_elevation_m": 54.0,
 "drilling_facility": {"name": "MÆRSK INSPIRER", "type": "JACK-UP 3 LEGS", "fixed_or_move": "FIXED"},
 "data_as_of": {"table": "wellbore_development_all", "exported_at_utc": "…", "sha256_16": "…"},
 "source": {"type": "sodir", "server": "sodir-mcp", "tool": "wellbore_facts",
            "query_id": "sodir_wellbore_v1", "params": {"wellbore": "15/9-F-12"}, "rows": 1, "…": "…"}}
```

`field_production_monthly(field="VOLVE", start="2014-01", end="2014-12")` returns `rows[{month,
oil_msm3, gas_bsm3, ngl_msm3, condensate_msm3, oe_msm3, water_msm3}]` as JSON numbers, `units`
(million Sm3, billion Sm3), `totals` summed in decimal, `data_coverage` and `notes`.

## What the answers will and will not say

- Values are exactly as Sodir publishes them; nothing is converted, completed or corrected.
- The completion date is Sodir's end of drilling for a development wellbore and rig release for an
  exploration wellbore drilled from a moveable facility; never the production start-up date. For a
  sidetrack the entered date is the first drilling after kick-off, not a spud (`date_notes`).
- `final_vertical_depth_tvd_m` null means "not published", never zero.
- The surface location is the wellhead, not the bottom hole.
- Facility names are quoted verbatim and may be cut at 16 characters by Sodir.
- Field volumes are whole-field saleable volumes; do not compare them with per-wellbore allocated
  volumes without stating both bases.
- Errors are structured: `{"error", "detail", "hint"}` with `invalid_input`, `unknown_wellbore`,
  `unknown_field`, `out_of_coverage`, `data_not_loaded`, `backend_unavailable`, `timeout`,
  `internal_error`. A name that is not an exact Sodir name or alias is never replaced by a
  similar one.

## Safety

Read-only tools; the cache is opened with `mode=ro`. No tool takes a path, a URL, SQL or a free-form
query. Names are validated (letters, digits, space and `/ . _ # : -`, 64 characters). Output is
capped at 500 rows and 20,000 characters. No credentials are involved.

## Postgres backend (optional, for the Volve Well Ops Copilot stack)

`pip install "sodir-mcp[postgres]"` and `SODIR_MCP_BACKEND=postgres` read the stack's `sodir_*`
tables as the read-only role (`POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`,
`POSTGRES_READONLY_USER`, `POSTGRES_READONLY_PASSWORD`). Same tools, same output.

## Development

```bash
cd mcp_servers/sodir
uv run --group dev pytest          # package tests (fixtures are hand-made CSVs)
uv build && uvx twine check dist/* # wheel and sdist; nothing is uploaded
```

From the repository root, `make test` and `make ci-full` run the package tests too (the root
`pyproject.toml` adds `mcp_servers/sodir/tests` to `testpaths` and `mcp_servers/sodir/src` to
`pythonpath`). The repository's `tests/mcp_servers/test_sodir_package.py` fails if the copied
helpers drift from `mcp_servers/common.py` or the output leaves `contracts/mcp-tools.md`.
