Skip to main content
Glama
manevpe

Sofia Data MCP

by manevpe
README.md
# Sofia Data MCP

TypeScript MCP server, `stdio` bridge, and CLI client for the Sofia municipality urban data portal at [https://urbandata.sofia.bg](https://urbandata.sofia.bg).

## Architecture

- Canonical server transport: Streamable HTTP
- Desktop compatibility transport: local `stdio` bridge that forwards to HTTP
- Local default endpoint: `http://127.0.0.1:3000/mcp`
- Source portal integration: CKAN Action API

## Connect an MCP Client

The deployed server is public, requires no auth, and speaks Streamable HTTP
at:

[https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp](https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp)

### Clients with native remote/HTTP MCP support (recommended)

**VS Code (Copilot Chat)** — add to `.vscode/mcp.json` in your project (or
your user `mcp.json` via the MCP: Open User Configuration command):

```json
{
  "servers": {
    "sofia-data": {
      "type": "http",
      "url": "https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp"
    }
  }
}
```

**Cursor** — add to `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "sofia-data": {
      "url": "https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp"
    }
  }
}
```

### Clients that only support local `stdio` servers (e.g. Claude Desktop)

Claude Desktop's `claude_desktop_config.json` only launches local processes,
so bridge to the remote HTTP endpoint with the widely-used `mcp-remote`
adapter (no install/build required, runs via `npx`):

```json
{
  "mcpServers": {
    "sofia-data": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp"
      ]
    }
  }
}
```

Alternatively, this repo's own `@sofia-data/stdio-bridge` package works the
same way — point it at the deployed URL instead of running the server
locally:

```bash
pnpm --filter @sofia-data/stdio-bridge dev -- \
  --upstream-url https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/mcp
```

### Quick smoke test (no client needed)

```bash
curl -s https://sofia-data-mcp-oparjiwnqa-ew.a.run.app/health
```

## Packages

- `@sofia-data/core`: shared CKAN client, normalization, analytics, and MCP tool definitions
- `@sofia-data/http-server`: HTTP MCP server
- `@sofia-data/stdio-bridge`: `stdio` bridge to an HTTP MCP server
- `@sofia-data/cli`: bilingual CLI client

## Requirements

- Node.js 24+
- Corepack available

## Install

```bash
corepack enable
corepack prepare pnpm@10.8.1 --activate
pnpm install
```

## Build

```bash
pnpm build
```

## Run Locally

Start the HTTP server:

```bash
pnpm dev:server
```

Default endpoint:

```text
http://127.0.0.1:3000/mcp
```

Run the CLI:

```bash
pnpm dev:cli
```

Run the `stdio` bridge:

```bash
pnpm dev:bridge
```

Override the bridge upstream URL:

```bash
pnpm --filter @sofia-data/stdio-bridge dev -- --upstream-url http://127.0.0.1:3000/mcp
```

## Docker

Build and run with Docker Compose:

```bash
docker compose up --build
```

## End-to-End Tests

`packages/e2e` runs a full black-box smoke test against a *deployed*
instance (health check, MCP `initialize` handshake, `listTools`, and a real
`search_datasets` call) — distinct from `packages/http-server`'s unit tests,
which spin up a local in-process server. It's skipped automatically (not
failed) when no target is given, so it never affects normal `pnpm test`/CI:

```bash
E2E_BASE_URL=https://sofia-data-mcp-oparjiwnqa-ew.a.run.app pnpm test:e2e
```

This also runs automatically as a post-deploy gate in
`.github/workflows/deploy.yml`, against the URL of the revision that was
just deployed.

## Deploying to Google Cloud Run

See [`deploy/gcp/README.md`](deploy/gcp/README.md) for scripts that deploy
`http-server` to Cloud Run (capped at 1 instance) with an automated budget
kill-switch that scales the service to zero if spend reaches a configured
threshold (default $2).

## Environment Variables

Copy `.env.example` and adjust as needed.

Key values:

- `SOFIA_CKAN_BASE_URL`
- `HOST`
- `PORT`
- `MCP_HTTP_PATH`
- `REQUEST_TIMEOUT_MS=30000`
- `PREVIEW_MAX_BYTES=262144`
- `MAX_SEARCH_RESULTS=50`
- `CACHE_TTL_MS=300000` — in-memory TTL cache for CKAN reads (groups, organizations, search results, dataset lookups). Set to `0` to disable caching.
- `ALLOWED_HOSTS` / `ALLOWED_ORIGINS` — comma-separated extra hostnames/origins (beyond `127.0.0.1`/`localhost`) allowed to reach the MCP endpoint. Required when deploying publicly (e.g. Cloud Run) so the server's own DNS-rebinding/CORS protection doesn't reject requests to its own public hostname. Left unset, the server only accepts local traffic.
- `MCP_UPSTREAM_URL`
- `MCP_SERVER_URL`

## Tools

- `search_datasets`
- `get_dataset`
- `list_groups`
- `get_group`
- `list_organizations`
- `get_organization`
- `list_dataset_resources`
- `preview_resource`
- `summarize_dataset`
- `find_related_datasets`
- `facet_datasets` — returns catalog-wide counts (group/organization/format/license) computed by CKAN's native faceting, not a client-side count over a capped page of results

## Dataset Metadata Enrichment

Dataset summaries include a `semanticProfile` field generated **generically** from CKAN
metadata alone (groups, tags, resource formats) — there is no per-dataset hardcoded
knowledge to maintain, so it scales automatically as datasets are added or renamed on the
portal. For exact field-level structure, use `summarize_dataset` or `preview_resource`,
which parse an actual sampled resource (JSON/GeoJSON) to report real geometry types and
property/field names.

Group names that were corrupted by a source-side Cyrillic transliteration/encoding bug
(e.g. `bnopa3hoo6pa3ne`) are automatically repaired using a generic transliteration of the
group's `title`, rather than a hardcoded name-to-name mapping.

## Testing

Each package has a unit test suite using Node's built-in test runner:

```bash
pnpm test
```

CI runs `pnpm build`, `pnpm check` (typecheck), and `pnpm test` on every push/PR to `main`
(see `.github/workflows/ci.yml`).

## Automated dependency updates

Dependabot (`.github/dependabot.yml`) checks weekly for updates to npm/pnpm packages,
GitHub Actions, and the Docker base image, grouping patch/minor updates into a single PR
per ecosystem. `.github/workflows/dependabot-auto-merge.yml` automatically approves and
enables auto-merge for patch/minor version PRs; the merge only completes once the
required `build-and-test` CI check passes on that PR (enforced by branch protection on
`main`). Major version bumps are left for manual review.

## Notes

- Technical naming is English-only.
- CLI display text is bilingual and rendered as two lines with English first.
- Source metadata from the portal is preserved as published.