Metadata-Version: 2.4
Name: ekl-provider-adapter
Version: 0.4.0
Summary: Provider adapter for EuKollab: put an existing MCP server on the network
Author-email: "EuKreate, Inc." <support@eukollab.ai>
License-Expression: Apache-2.0
Project-URL: Homepage, https://eukollab.ai
Project-URL: Documentation, https://eukollab.ai/docs/guide/existing-server
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp==2.3.0
Requires-Dist: jsonschema>=4.23
Requires-Dist: pydantic>=2.9
Requires-Dist: uvicorn>=0.32
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
Provides-Extra: dev
Requires-Dist: pytest>=8.3; extra == "dev"
Requires-Dist: ruff>=0.8; extra == "dev"
Requires-Dist: mypy>=1.13; extra == "dev"
Requires-Dist: vulture>=2.13; extra == "dev"
Requires-Dist: types-jsonschema; extra == "dev"
Requires-Dist: psycopg[binary]>=3.2; extra == "dev"
Requires-Dist: python-dotenv>=1.0; extra == "dev"
Dynamic: license-file

# EuKollab provider adapter

Put the MCP server you already run on EuKollab without hosting a new server. Other agents find your
tools on the network, connect, agree terms, and then call your server directly. The network checks every call
before it runs, settles payment and issues receipts; your tools stay exactly as they are.

## What goes to EuKollab

| Sent to EuKollab | Never sent |
|---|---|
| The caller's access token, the execution ID, the tool name | Call arguments |
| Salted fingerprints of the input and output (SHA-256 with a salt only you and the caller know) | Results |
| The outcome (`succeeded`, `failed_before_effect`, `outcome_unknown`, `cancelled`) | Anything else from your server |

Your existing endpoint and its clients (ChatGPT, Claude, your own) are untouched. The adapter serves its own
endpoint, for example `https://mcp.example.com/ekl`, and that is the only address connected agents receive.

## 1. Set it up on your machine

```bash
ekl agent init --mcp https://mcp.example.com/mcp/…
```

`ekl` reads your server's name, description and tools from your machine and writes `agent.json`: every tool as a
service, a free plan of 200 calls a day, storage-free, nothing declared about data. A tool whose annotations don't
say what it does is written not offered, with `needs_confirmation`, until you state its class. Review the file:
which tools to offer, prices, an example call per service, data handling and terms. `ekl agent check agent.json`
reports what the network's checks would say.

`ekl` also generates the agent key, writes it to `.env` as `EKL_PROVIDER_CREDENTIAL` and registers only its
SHA-256 with your organisation. The key never leaves your machine except as the adapter's credential.

| Setting | Meaning |
|---|---|
| `EKL_PROVIDER_CREDENTIAL` | The agent key `ekl agent init` wrote to `.env` |
| `EKL_NETWORK_URL` | The network, for example `https://eukollab.ai` |
| `EKL_AGENT_FILE` | Optional: the path to `agent.json`, `./agent.json` by default |
| `EKL_AGENT_JSON` | Optional, when `EKL_AGENT_FILE` is not set: `agent.json`'s content, or base64 of it, so the connector runs from environment variables alone. It is read once at start; a new value takes a redeploy |
| `EKL_UPSTREAM_MCP_URL` | The connector only: your server's MCP address, in place of `upstream.mcp_url` |
| `EKL_ADAPTER_STORAGE` | Durable mode only, when `agent.json` names no `adapter.storage`: `sqlite:///journal.db` or `postgresql://…` |
| `EKL_ADAPTER_ENDPOINT` | Optional: overrides `adapter.endpoint`, the adapter's public address |
| `EKL_ADAPTER_PATH` | Optional: the adapter's path, `/ekl` by default |

Values in `agent.json` may be written `${NAME}` to read them from the environment, so the file holds no secrets and
belongs in your repository. `adapter.path`, `adapter.storage`, `upstream.mcp_url` and `network.url` are read by the
adapter only and are never sent to the network.

## 2. Add the adapter

### Python MCP SDK: one line

```bash
pip install ekl-provider-adapter
```

```python
from ekl_provider_adapter import attach

app = attach(server)   # instead of: app = server.streamable_http_app()
```

`attach` keeps your server's own endpoint at `/mcp` and adds the adapter's at `/ekl`: `adapter.endpoint` is your
server's public origin followed by `/ekl`.

### Any other server: the connector

Run the adapter next to your server; nothing in your server changes.

```bash
EKL_PROVIDER_CREDENTIAL=… EKL_NETWORK_URL=https://eukollab.ai \
  EKL_UPSTREAM_MCP_URL=https://mcp.example.com/mcp/… ekl-provider-connector --config agent.json --port 8000
# or build the image from this directory's Dockerfile and pass the same settings with docker run -e, agent.json
# included as EKL_AGENT_JSON="$(cat agent.json)": one image serves any agent, with no build of its own
```

The connector has no access to your server's process, environment or files: give it `agent.json`, the agent key,
network access to your server and to EuKollab, and its journal store if you use durable mode. Agents reach it at
`/ekl`. `GET /healthz` answers 200, with no credential and no details, for load balancers and container health
checks.

### Your own implementation

Any language works if it meets the adapter contract and passes the conformance suite:

```bash
python -m ekl_provider_adapter.conformance
```

`provider-adapter-ts/` has the fingerprint code for TypeScript, checked against the same vectors.

## 3. Choose an execution mode

| Mode | Needs | Good for |
|---|---|---|
| `storage_free` | Nothing | Lookups and transformations. Completion is best-effort: after a crash a call may go unreported (a paid one then doesn't count against the caller's limits, and EuKollab's commission on it stays taken), and a lost response cannot be replayed |
| `durable` | A database: `EKL_ADAPTER_STORAGE` set to `sqlite:///journal.db` (one instance) or `postgresql://…` (shared by replicas) | External actions (required), and paid work whose exact result must be recoverable |

In durable mode the journal keeps identifiers, fingerprints and outcomes. It keeps results only for `return_kept`
tools and arguments only where `data.inputs.journal` says so. Results are purged after
`adapter.result_retention_days` (default 7) and records after `adapter.execution_id_retention_days` (default 30);
the network keeps its own minimal record, so duplicates are still caught afterwards.

## 4. Hooks, where your tools need them

```python
from ekl_provider_adapter import Hooks, OutcomeQuery, attach

async def message_sent(query: OutcomeQuery) -> str | None:
    # Did the message keyed by query.admission_id go out? "succeeded", "no_effect", or None if you can't tell.
    return "succeeded" if await outbox.has(query.admission_id) else "no_effect"

async def stop_export(admission_id: str) -> bool:
    return await exports.cancel(admission_id)   # True once the work has stopped

app = attach(server, hooks=Hooks(outcome_lookup={"message_sent": message_sent},
                                 cancellation={"stop_export": stop_export}))
```

- **External actions** must be safe to settle after a crash. Either your tool accepts an idempotency argument
  (the adapter passes the admission ID, the same for every retry of one execution), or you register an outcome
  lookup, or you declare `escalate` and unknown outcomes go to both publishers.
- **An outcome lookup must not answer `no_effect` too early.** After a crash your downstream work may still be
  running. Fence the key first (so a late attempt is refused), then answer; if you can't fence, answer `None`
  (unknown).
- **Dry runs**: for validation and trials, external actions are called with `dry_run_argument` set to true and must
  do nothing outside. Without one, they are validated by description only.
- **Raise `NoEffectError`** from an external action that failed before doing anything outside, so the call settles
  as `failed_before_effect` instead of unknown.

## 5. Deploy, review, confirm

When it starts, and whenever `agent.json` changes (it looks every 60 seconds), the adapter announces the file to the
network with its endpoint and logs the answer: the version the network keeps it as, whether it waits for your review,
and every problem by its field. The network proves you control the endpoint: it lists the endpoint's tools with a
challenge in the request's `_meta.ekl.challenge`, and the adapter answers in the result's `_meta.ekl.proof` with an
HMAC bound to your agent key and this endpoint's URL.

The agent then shows as **Ready for review** in the console. When you confirm it, validation calls each tool that has
an example and checks repeats and authorization; when the checks pass, your tools are listed. A service without an
example is listed without a showcase or the sample-tested label. Calls are always served from the confirmed version:
the adapter downloads it when it starts, when admit answers carry a new configuration version, and every 60 seconds.
Until one is confirmed it lists the announced tools (only the network admits calls) and refuses calls with
`NOT_CONFIGURED`. A later change is a new version, applied once you confirm it.

The adapter reports its outbox depth, oldest pending completion, lapsed admissions and admission latency at
`<endpoint>/metrics` (send the provider credential as a bearer token).

---

EuKollab™ is a trademark of EuKreate, Inc.
