Skip to main content
Glama
milad13711
by milad13711
README.md
# Exir MCP Server

A multi-tenant [Model Context Protocol](https://modelcontextprotocol.io) gateway that sits between MCP hosts (Claude, ChatGPT, or any other MCP-compatible agent) and the Exir CRM API (Perfex CRM under the hood).

```
                    ┌──────────────────────┐
                    │   ChatGPT / Agent     │
                    │ Claude / Other Host   │
                    └──────────┬────────────┘
                               │
                         MCP / HTTPS
                               │
                               ▼
              ┌────────────────────────────────┐
              │        Exir MCP Server          │
              │                                 │
              │ OAuth 2.1 / OIDC                │
              │ Tenant Resolver                 │
              │ Permission Engine               │
              │ Tool Registry                   │
              │ Audit Logger                    │
              │ Rate Limiter                    │
              │ Input Validation                │
              └───────────────┬─────────────────┘
                               │
                        Internal API
                               │
                               ▼
              ┌────────────────────────────────┐
              │          Exir CRM API           │
              │                                 │
              │ Customers / Leads / Sales       │
              │ Tasks / Projects / Tickets      │
              │ Invoices / Contracts / ...      │
              └───────────────┬─────────────────┘
                               │
                               ▼
                    ┌──────────────────┐
                    │ Customer Tenant   │
                    │ Data / Database   │
                    └──────────────────┘
```

## How a request flows

1. **Transport** — an MCP host sends a `POST /mcp` (Streamable HTTP) request with a `Bearer` access token.
2. **OAuth 2.1 / OIDC** (`src/auth/oidc.ts`) — the token's signature, issuer, audience and expiry are verified against the identity provider's JWKS. Unverified tokens never reach anything below this layer.
3. **Tenant Resolver** (`src/tenant/tenantResolver.ts`) — the tenant id is read from a claim on the *verified* token and mapped to that tenant's Exir CRM connection (base URL + API key). Requests can never cross tenant boundaries.
4. **Rate Limiter** (`src/middleware/rateLimiter.ts`) — requests are throttled per tenant so one noisy caller cannot starve another on a shared deployment.
5. **Tool Registry** (`src/tools/`) — the MCP server for this request is built by wrapping every registered tool (`src/mcp/server.ts`) with:
   - **Permission Engine** (`src/permissions/permissionEngine.ts`) — checks the token's OAuth scopes against the tool's required scope.
   - **Input Validation** — every tool declares a [zod](https://zod.dev) schema; invalid input is rejected before it reaches the CRM.
   - **Audit Logger** (`src/audit/auditLogger.ts`) — every call (allowed, denied, success, or error) is recorded with tenant, subject, tool name, and outcome.
6. **Internal API** (`src/crm/perfexClient.ts`) — a tenant-scoped HTTP client calls the real Exir CRM API (Perfex CRM's REST API, `authtoken` header auth) and returns the result back up the chain as the tool's output.

## Project layout

```
src/
  auth/oidc.ts            OAuth 2.1 / OIDC bearer-token verification
  tenant/tenantResolver.ts Tenant lookup + per-tenant CRM connection details
  permissions/permissionEngine.ts  Scope-based authorization
  audit/auditLogger.ts     Structured audit trail for every tool call
  middleware/rateLimiter.ts Per-tenant rate limiting
  crm/perfexClient.ts      Internal API client to the Exir CRM API
  tools/                   Tool Registry + one file per CRM domain
    customers.ts leads.ts tasks.ts invoices.ts
  mcp/server.ts            Wires tools -> permissions -> validation -> audit -> CRM
  http/app.ts              Express app: /healthz, POST /mcp
  index.ts                 Process entrypoint
tests/                     Vitest unit tests (permission engine, tool registry)
```

## Getting started

```bash
npm install
cp .env.example .env   # fill in OIDC_ISSUER, CRM_API_BASE_URL, CRM_API_KEY, ...
npm run dev             # ts-node/tsx dev server on :3333
```

Build & run for production:

```bash
npm run build
npm start
```

Or via Docker:

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

Run tests:

```bash
npm test
```

## Adding a new tool

1. Add a `registry.register({...})` call in the relevant file under `src/tools/` (or a new file for a new CRM domain), with a `name`, `description`, `requiredScope`, a `zod` `inputSchema`, and a `handler(crm, input)` that calls the `PerfexClient`.
2. If it's a new file, wire it into `buildToolRegistry()` in `src/tools/index.ts`.
3. Add a test in `tests/tools.test.ts` asserting the tool is registered and its schema rejects bad input.

No changes are needed anywhere else — permissioning, validation, and auditing are applied generically to every registered tool by `src/mcp/server.ts`.

## Multi-tenancy

`src/tenant/tenantResolver.ts` ships with an `EnvTenantStore` dev fallback that resolves every tenant id to the single CRM connection in `.env`. For real multi-tenant deployments, implement the `TenantStore` interface against your tenant directory (Postgres, a config service, etc.) mapping tenant id -> `{ baseUrl, apiKey }`, and pass it into `tenantResolver(myStore)` in `src/http/app.ts`.

## Security notes

- The tenant id is only ever trusted from a claim on a cryptographically verified access token, never from a client-supplied header, unless `TENANT_HEADER_FALLBACK=true` is explicitly set for trusted internal-network callers (e.g. local development).
- Per-tenant CRM API keys are never logged (`src/logger.ts` redacts `Authorization`, `*.apiKey`, `*.token`).
- Every tool call is validated twice: once by the MCP SDK against the tool's JSON schema, and again by `zod.safeParse` inside the handler, before any CRM API call is made.
- The server runs statelessly (`sessionIdGenerator: undefined`): a new MCP server instance is created per HTTP request, scoped to that request's verified tenant and permissions, so there is no shared session state that could leak across tenants.