Skip to main content
Glama
Randika97

MCP Integration Bridge

by Randika97
README.md
# MCP Integration Bridge

An MCP server that connects **any two systems** so an agent can explore both,
map between them, and run real data transfers — then shows its working in a run
folder you can audit afterwards.

Nothing in the codebase names a product. Which systems the bridge talks to is
decided by a `.env` file (addresses and credentials) and a **profile** (the shape
of each system: its endpoints, entities, queries, and documentation tree).
Pointing the bridge at a different pair of systems is a configuration change.

```mermaid
flowchart LR
  MCP["MCP client<br/>(Cursor, Claude Desktop)"] -- stdio --> B
  UI["Browser UI"] -- HTTP --> B
  B["Integration bridge"] <--> SRC["SOURCE system<br/>records read from"]
  B <--> TGT["TARGET system<br/>records written to"]
  B <--> HUB["HUB (optional)<br/>orchestration API"]
  ENV[".env"] --> B
  PROF["profiles/&lt;id&gt;/"] --> B
```

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design.

---

## What it gives an agent

| Capability             | How                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------- |
| **Explore live APIs**  | Authenticated tools for each system: REST/OData reads, GraphQL queries, introspection |
| **Explore the docs**   | Offline keyword search across each system's source or specification tree              |
| **Read named entities**| `source_query_data` / `target_query_data` over operations the profile declares        |
| **Transfer records**   | `execute_workflow` runs forward, reverse, or a full round trip                        |
| **Self-bootstrap**     | A missing workflow is built from a template, a codegen pipeline, or generated modules |
| **Audit everything**   | Every tool call, payload, and skipped field lands in a timestamped run folder         |

---

## Quickstart

```bash
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt   # Linux/macOS: .venv/bin/pip

cp .env.example .env      # then fill in the SOURCE_* and TARGET_* values
python scripts/selfcheck.py
```

`selfcheck.py` validates the profile, catalog, workflow and ingest wiring without
contacting either system, so it works before you have credentials.

**As an MCP server** (stdio) — copy `mcp.json.example` into your MCP client
config and adjust the paths:

```bash
.venv/Scripts/python server.py
```

**As an HTTP API** for a browser UI:

```bash
.venv/Scripts/python http_server.py     # http://127.0.0.1:8765
```

Start with `bridge_info` — it reports the active profile, both roles, and exactly
which environment variables are still missing.

---

## Configuration

Every connected system is a **role**, and all roles are configured the same way:
`<ROLE>_<OPTION>`, where the role is `SOURCE`, `TARGET`, or `HUB`. Environment
variables override the profile's defaults, so a profile ships the shape of a
system and `.env` supplies the instance.

### Minimum viable `.env`

```ini
ACTIVE_PROFILE=erp-to-tms

SOURCE_BASE_URL=https://your-tenant.example.com
SOURCE_TOKEN_URL=https://your-tenant.example.com/auth/realms/main/protocol/openid-connect/token
SOURCE_CLIENT_ID=your-client-id
SOURCE_CLIENT_SECRET=your-client-secret
SOURCE_DOCS_PATH=c:/Repositories/your-erp/workspace

TARGET_BASE_URL=https://your-platform.example.com
TARGET_USERNAME=you@example.com
TARGET_PASSWORD=your-password
TARGET_DOCS_PATH=c:/Repositories/your-platform
```

`.env.example` documents the full surface, including connector selection, path
prefixes, login form field names, bootstrap behaviour, and the HTTP bridge.

### Connectors

A role picks its protocol with `<ROLE>_CONNECTOR`:

| Value             | Authentication                     | Suits                                     |
| ----------------- | ---------------------------------- | ----------------------------------------- |
| `oauth2_rest`     | OAuth2 client credentials → bearer | REST / OData service catalogues           |
| `session_graphql` | CSRF-protected form login → cookie | GraphQL web applications                  |
| `token_api`       | Credentials → JWT bearer           | JSON APIs that issue a token from a login |

### Searching documentation

Each role's offline search is described entirely by settings, so it works against
a source checkout, a specification bundle, or a folder of docs:

```ini
SOURCE_DOCS_PATH=c:/Repositories/your-erp/workspace
SOURCE_DOCS_GLOBS=*/model/**/*.projection,*/model/**/*.entity
SOURCE_DOCS_INDEX_GLOB=*/model/**/*.projection

TARGET_DOCS_PATH=c:/Repositories/your-platform
TARGET_DOCS_GLOBS=**/graphql/*.py,**/schema.py
TARGET_DOCS_INDEX_FILE=your_app/schema.py
TARGET_DOCS_INDEX_REGEX=(\w+Query)
```

`DOCS_GLOBS` selects what is searchable; the index settings produce the list of
named API surfaces that `*_search_docs` returns alongside raw matches.

---

## MCP tools

**Bridge and runs**

| Tool          | Purpose                                                            |
| ------------- | ------------------------------------------------------------------ |
| `bridge_info` | Active profile, both roles, paths, and what is still unconfigured  |
| `run_new`     | Start a new run folder                                             |
| `run_info`    | Active run id, folder, and counters                                |

**Workflows**

| Tool                       | Purpose                                                     |
| -------------------------- | ----------------------------------------------------------- |
| `list_workflows`           | Registered workflows and whether each is ready              |
| `workflow_bootstrap_hints` | Search hints and a starter manifest for a missing workflow  |
| `register_workflow`        | Register a manifest plus `forward.py` and optional `reverse.py` |
| `bootstrap_workflow`       | Build a workflow automatically                              |
| `execute_workflow`         | Run `forward`, `reverse`, or `round_trip`                   |

**Source role** — `source_connection_info`, `source_search_docs`, `source_http`,
`source_service_query`, `source_service_metadata`, `source_query_data`

**Target role** — `target_connection_info`, `target_search_docs`,
`target_graphql`, `target_introspect`, `target_query_data`

**Hub role** — `hub_connection_info`, `hub_login`, `hub_trigger_run`,
`hub_poll_run`, `hub_answer_run`, `hub_graphql`, `hub_http`

### Example agent flow

```
bridge_info                                        → confirm both roles are configured
source_search_docs("Shipment")                     → find the source API surface
target_introspect()                                → see what the target accepts
source_query_data("shipments", limit=5)            → sample real records
execute_workflow(workflow_id="booking", source_id="12345")
run_info                                           → the folder holding the evidence
```

---

## Profiles

A profile describes one concrete pair of systems. `profiles/erp-to-tms/` ships as
a worked reference — copy it, edit it, and set `ACTIVE_PROFILE`.

```
profiles/<id>/
  profile.json     role defaults (connector, path prefixes, docs globs) + workflow manifests
  catalog.json     named read operations behind source_query_data / target_query_data
  ingest.json      write operations that generated mappings call
  reconcile.json   entities to compare across both systems
  smoke.json       the end-to-end pipeline and its pass criteria
  discovery.json   discovery step labels and the outcome catalogue
  templates/       bundled forward.py / reverse.py for bootstrap
```

Only `profile.json` is required.

### Adding a readable entity

```json
"source": { "operations": {
  "purchase_orders": {
    "description": "Purchase order headers.",
    "service": "PurchaseOrderHandling",
    "entity_set": "PurchaseOrderSet",
    "filter_template": "OrderNo eq '{identifier}'",
    "search_field": "Description"
  }
}}
```

`source_query_data("purchase_orders", identifier="PO-1")` works immediately, with
no code change.

### Adding a write operation

Generated mappings call methods that no Python file defines — the adapter
resolves the name against `ingest.json` at call time:

```json
"operations": {
  "create_order": {
    "document": "mutation CreateOrder($input: OrderInput!) { createOrder(input: $input) { order { id state } } }",
    "wrap_positional": "input",
    "variables": { "input": "{input}" },
    "root": "createOrder.order",
    "required": true,
    "flatten": { "id": "id", "state": "state" }
  }
}
```

Mapping logic too complex to express declaratively belongs in the workflow's
`forward.py` / `reverse.py`, which is ordinary Python. That is the intended
boundary: profiles describe *what the systems offer*; workflows describe *how
this business mapping works*.

---

## Workflows and runs

A workflow is a manifest (how to fetch from the source, what the target entity is
called, whether a reverse callback exists) plus a `forward.py` and optional
`reverse.py`. Asking for one that does not exist is not an error — the bridge
tries a bundled template, then a codegen pipeline, then modules already generated
under `CODEGEN_ROOT`, and only then returns `needs_bootstrap` with search hints
and a starter manifest.

Every tool call runs inside a run folder:

```
runs/2026-08-11/143022-booking-12345/
  run.json  tool_calls.jsonl  transfer_log.jsonl  skips.jsonl
  inputs/   outputs/   mappings/
```

---

## HTTP bridge

For browser UIs that cannot speak MCP stdio. Routes are role-shaped:

| Method | Path                                          | Purpose                             |
| ------ | --------------------------------------------- | ----------------------------------- |
| GET    | `/api/health`, `/api/info`                    | Liveness and active configuration   |
| GET    | `/api/workflows`, `/api/runs`                 | Registry and run history            |
| POST   | `/api/sessions`                               | Create a session                    |
| POST   | `/api/sessions/{id}/configure`                | Supply credentials                  |
| POST   | `/api/sessions/{id}/configure-env`            | Use the server's `.env`             |
| GET    | `/api/sessions/{id}/source/test`, `/target/test` | Prove each connection            |
| GET    | `/api/sessions/{id}/source/services`, `/target/types` | Discovered API surfaces      |
| POST   | `/api/sessions/{id}/discovery/start`          | Walk both systems                   |
| POST   | `/api/sessions/{id}/reconcile/run`, `/reconcile/lookup` | Master-data comparison    |
| POST   | `/api/sessions/{id}/smoke/run`                | End-to-end proof                    |
| POST   | `/api/sessions/{id}/agent/*`                  | Agent-planned discovery and builds  |

---

## Repository layout

```
server.py                MCP entry point (stdio)
http_server.py           HTTP bridge entry point
integration_mcp/
  config/                .env loading, profiles, per-role settings
  connectors/            oauth2_rest, session_graphql, token_api, graphql_ingest
  catalog.py             profile-declared read operations
  search/                offline documentation and source-tree search
  transfer/              workflow execution
  workflows/             registry, manifests, bootstrap strategies
  runs/                  run folders, tool-call logging, spec snapshots
  http/                  sessions, discovery, reconcile, smoke, agent
profiles/                per-integration configuration
workflows/               registered forward/reverse modules (gitignored)
runs/                    run artefacts (gitignored)
scripts/                 selfcheck.py
docs/ARCHITECTURE.md     design, diagrams, extension points
```

---

## Security notes

- Each connector may only reach the hosts it was configured with. Add more with
  `<ROLE>_ALLOWED_HOSTS`, which keeps an authenticated connector from becoming an
  SSRF primitive.
- Credentials are read from the environment only; `.env` and `.env.*` are
  gitignored. Cached cookies and tokens live under `STATE_DIR`.
- `*_connection_info` tools report whether a credential is present, never its
  value.
- Writing back to the source system is off by default; it requires
  `post_callback=true` or `TRANSFER_POST_CALLBACK=1`.