Skip to main content
Glama
vaulcent

EasyTouch MCP

by vaulcent
README.md
# EasyTouch MCP

EasyTouch MCP V1 is a public, multi-tenant, read-only gateway from MCP-compatible AI clients to each customer's own ERPNext/Frappe site. One deployment serves many customers without putting ERP credentials in tool arguments, model context, source code, or per-customer environment variables.

## Security and architecture

```text
MCP client -> OAuth authorization server -> Bearer token
    -> EasyTouch MCP resource server (/mcp)
    -> issuer + subject + tenant claim
    -> exact PostgreSQL tenant/user connection lookup
    -> decrypt ERP secret for this request only
    -> HTTPS Frappe REST call with token key:secret
    -> permission-filtered response
```

The MCP server is an OAuth 2.1 resource server. It does not implement login or issue tokens. The pluggable `OAuthVerifier` uses RFC 7662 introspection and checks token activity, expiry, required scope, audience, subject, and the configured tenant claim. The official SDK publishes RFC 9728 protected-resource metadata for `/mcp`.

Customer connections are unique by `(tenant_id, user_id)`. PostgreSQL stores the ERP URL and API key plus a Fernet-encrypted API secret. The platform `ENCRYPTION_KEY` is the only decryption key. It should come from a secret manager and be backed up separately; losing it makes stored credentials unrecoverable. Rotation requires decrypting with the old key and re-encrypting with the new key in a controlled job.

Outbound ERP traffic permits only:

- `GET /api/resource/{DocType}`
- `GET /api/resource/{DocType}/{name}`
- `GET /api/method/frappe.client.get_count`
- `GET /api/method/frappe.desk.form.load.getdoctype`
- `GET /api/method/frappe.desk.query_report.run`
- `GET /api/method/frappe.auth.get_logged_user`

No generic write tool or write method exists. Redirects are disabled. ERP hostnames are normalized, DNS-resolved and rejected if any address is non-global in production; localhost, private, link-local, loopback, multicast, reserved and unspecified targets are blocked. Re-checks happen before every ERP request. Deploy behind an HTTPS reverse proxy with egress firewall rules as an additional SSRF control.

Frappe receives the customer's own `Authorization: token API_KEY:API_SECRET`, so Frappe's roles, user permissions, document sharing, field-level permissions, and report permissions remain authoritative. EasyTouch does not use `ignore_permissions`.

The built-in limiter is deliberately a single-process structure. Replace it with a Redis-backed implementation before scaling to multiple replicas. Audit records never contain filters, results, tokens, API keys, or secrets.

## Tools

The six tools are `list_documents`, `get_document`, `search_documents`, `count_documents`, `get_doctype_meta`, and `run_report`. Limits, names, fields, filters, ordering, URL components, response size, and report inputs are validated. `search_text` V1 searches the document `name`; use explicit filters for other fields.

## Install and run locally

Python 3.11+ is required (the container uses 3.12).

```bash
cp .env.example .env
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e '.[test]'
docker compose up -d postgres
python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
```

Put the generated value in `.env` as `ENCRYPTION_KEY`. For local auth, generate a random `DEV_ACCESS_TOKEN` (for example `openssl rand -hex 32`). It is accepted only when `APP_ENV` is not `production`. Then migrate and run:

```bash
alembic upgrade head
uvicorn app.main:app --host 127.0.0.1 --port 8000
```

Health check:

```bash
curl -sS http://127.0.0.1:8000/health
```

## Connect an ERPNext customer

In production, obtain an EasyTouch access token from the configured OAuth provider. Its token/introspection response must contain `sub`, the `tenant_id` claim (configurable), the `easytouch:read` scope, and an audience for `MCP_PUBLIC_URL`. Submit the ERP credentials directly to the protected onboarding API; this response never includes the secret:

```bash
curl -sS http://127.0.0.1:8000/api/v1/connections/erpnext \
  -H "Authorization: Bearer $EASYTOUCH_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"erp_url":"https://erp.customer.com/desk","api_key":"API_KEY","api_secret":"API_SECRET"}'
```

The service validates the credentials with `GET /api/method/frappe.auth.get_logged_user`, then encrypts and saves them. Never put credentials in shell history in production; the customer portal should send this request over HTTPS.

For direct real-ERP local testing without inserting a database connection, set `DEV_ERP_URL`, `DEV_ERP_API_KEY`, and `DEV_ERP_API_SECRET`. This bypass exists only outside production and only for the configured development tenant/user.

## Test

```bash
pytest -q
```

To verify authenticated transport and tool discovery with the included official-SDK smoke client:

```bash
python scripts/smoke_mcp.py http://127.0.0.1:8000/mcp --token "$EASYTOUCH_ACCESS_TOKEN"
```

Or use the MCP Inspector:

```bash
npx @modelcontextprotocol/inspector http://127.0.0.1:8000/mcp \
  --header "Authorization: Bearer $EASYTOUCH_ACCESS_TOKEN"
```

An MCP-compatible client uses `https://mcp.easytouch.so/mcp`. It discovers the authorization server from `https://mcp.easytouch.so/.well-known/oauth-protected-resource/mcp`, completes OAuth with that provider, and sends the resulting bearer token on every MCP HTTP request.

## Production deployment

1. Provision PostgreSQL with TLS, backups, restricted network access and a least-privilege database user.
2. Configure `APP_ENV=production`, a secret-manager-backed `ENCRYPTION_KEY`, real OAuth introspection credentials, `MCP_PUBLIC_URL=https://mcp.easytouch.so/mcp`, and exact `ALLOWED_HOSTS`/`TRUSTED_ORIGINS`.
3. Run `alembic upgrade head` as a release task.
4. Build and run the non-root Docker image behind a TLS reverse proxy. Restrict outbound port 443 and add network-level private-range denial.
5. Replace the in-memory rate limiter with shared Redis before adding replicas. Send sanitized application logs and database audit events to the platform SIEM.

The production OAuth provider still needs to be selected and configured. It must support OAuth 2.1 client flows used by MCP clients, RFC 8414 metadata, RFC 9728 resource discovery, PKCE, resource indicators/audience binding, and the current Client ID Metadata Document flow. Dynamic Client Registration is legacy compatibility in the 2026-07-28 MCP revision. The MCP service itself intentionally remains only the resource server, as recommended by the current SDK.

## Official sources verified for this implementation

- [MCP 2026-07-28 specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28/)
- [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) (the stable authorization page; 2026 hardening is summarized in the release above)
- [Official Python SDK authorization guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/run/authorization.md)
- [Official Python SDK ASGI guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/run/asgi.md)
- [Frappe REST API and token authentication](https://docs.frappe.io/framework/user/en/api/rest)
- [Frappe read client methods](https://github.com/frappe/frappe/blob/develop/frappe/client.py)