Skip to main content
Glama
README.md
# Universal API MCP

**Let Claude call any API — GitHub, Notion, Linear, Stripe, Slack, Jira, AWS and more — without ever seeing your tokens.**

Universal API is a remote [MCP](https://modelcontextprotocol.io) server you deploy once on Cloudflare Workers (free plan). Your API keys live in [Infisical](https://infisical.com), a secrets vault. When Claude needs to call an API, it passes only the secret's **name**. The server looks up the real token, makes the call, removes the token from the response, and hands back just the result.

No more pasting tokens into chat. No token in the model's context, the chat log, or any prompt.

```
You:    "List my open GitHub issues and add each one to my Notion task board."
Claude: api_request { url: "https://api.github.com/issues", secret: "GITHUB_TOKEN" }
        api_request { url: "https://api.notion.com/v1/pages", method: "POST", secret: "NOTION_TOKEN", ... }
        → Done. 7 issues added.        (Claude never saw either token.)
```

---

## Contents

- [Why use it](#why-use-it)
- [How it works](#how-it-works)
- [Tools](#tools)
- [Setup (about 20 minutes)](#setup-about-20-minutes)
- [Connect it to Claude](#connect-it-to-claude)
- [Add your API tokens](#add-your-api-tokens)
- [Using it](#using-it)
- [Approvals for risky actions](#approvals-for-risky-actions)
- [Optional extras](#optional-extras)
- [Security model and limits](#security-model-and-limits)
- [Troubleshooting](#troubleshooting)
- [Local development](#local-development)
- [Project layout](#project-layout)
- [License](#license)

---

## Why use it

| Without Universal API | With Universal API |
|---|---|
| You paste a token into the chat, and it sits in the conversation history | Tokens stay in your vault, and Claude only knows their names |
| One MCP server per service (GitHub MCP, Notion MCP, Linear MCP, ...) | **One server for every HTTPS API** |
| A prompt-injected page could trick the model into sending your token somewhere else | Each token is **locked to its own hosts**; a GitHub token can only ever go to `api.github.com` |
| Deletes happen as soon as the model decides | **Deletes and overwrites need your click** on an approval page |
| Adding a new service means new code or a new server | Add a token with its hosts. **No redeploy.** |

**Good for:** anyone who uses Claude (claude.ai, Claude Desktop, Claude Code) with real accounts and wants it to work across many services safely.

---

## How it works

```mermaid
sequenceDiagram
    participant C as Claude
    participant W as Universal API (your Cloudflare Worker)
    participant V as Infisical (your vault)
    participant A as Any API (e.g. api.github.com)
    C->>W: api_request { url, secret: "GITHUB_TOKEN" }
    W->>V: read GITHUB_TOKEN + its allowed hosts
    W->>W: is api.github.com an allowed host? is it destructive?
    W->>A: HTTPS request with the token added
    A-->>W: response
    W-->>C: response with the token redacted
```

- **Sign-in:** the server sits behind Google sign-in. Only emails you list in `ALLOWED_EMAILS` can connect.
- **Vault:** secrets are read live from Infisical, so you add or rotate a token there and it works right away.
- **Host lock:** every secret carries a `hosts:` line saying where it may be sent. No line, no sending.

---

## Tools

Once connected, Claude sees these tools. You don't call them yourself; just ask Claude in plain language.

### Calling APIs

| Tool | What it does | Needs your approval? |
|---|---|---|
| **`api_request`** | The main tool. Makes an authenticated HTTPS call to any API using a stored secret, and returns the response with the secret redacted. Supports JSON bodies, query params, custom headers, file uploads (up to 5 MB) and AWS SigV4 signing. | Only for destructive calls (see [Approvals](#approvals-for-risky-actions)) |

**Auth types** supported by `api_request`:

| `auth_type` | Sends | Use for |
|---|---|---|
| `bearer` (default) | `Authorization: Bearer <token>` | GitHub, Notion, Stripe, Slack, OpenAI, most APIs |
| `header` | `<header_name>: [<header_prefix> ]<token>` | APIs with a custom header, e.g. `X-API-Key`, or Linear (`Authorization: <key>`) |
| `basic` | `Authorization: Basic ...` (secret is `user:password`, or pass `username` and store just the password) | Jira / Confluence (`email:api_token`), Twilio |
| `query` | `?<query_param>=<token>` | APIs that take the key in the URL |
| `aws_sigv4` | AWS Signature V4, from secrets `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (and optional `AWS_SESSION_TOKEN`) | Any AWS API: S3, STS, EC2, ... |

### Managing your secrets

| Tool | What it does | Needs your approval? |
|---|---|---|
| `list_secrets` | Lists secret names, folders, notes and the hosts each may be sent to. **Never values.** | No |
| `get_secret` | Shows one secret's details. **Never the value.** | No |
| `add_secret` | Stores a new token with the hosts it may be sent to | No |
| `update_secret` | Changes a secret's note, value or name (hosts never change) | Yes, for a new value or name |
| `delete_secret` | Deletes a secret permanently | **Always** |
| `list_folders` | Lists folders in the vault | No |
| `list_environments` | Lists vault environments (e.g. `dev`, `prod`) | No |

### Optional extras

| Tool | What it does | Needs your approval? |
|---|---|---|
| `get_upload_url` | For big files (up to 5 GB): gives a one-time upload link and a temporary download link to pass to other APIs. The file never goes through the chat. | No |
| `issue_temp_token` | Gives Claude a short-lived (1 hour max) vault token for real CLIs like `aws` or `terraform` | **Always**, with a must-tick box |
| `access_guide` | Setup help text for `issue_temp_token` | No |

`get_upload_url` and `issue_temp_token` need extra setup; until then they reply with the setup steps. See [Optional extras](#optional-extras).

---

## Setup (about 20 minutes)

### What you need

- A **Cloudflare** account (free plan is fine)
- An **Infisical** account (free plan is fine), on US or EU cloud
- A **Google Cloud** project (for the sign-in screen; free)
- **Node.js 20+** and **git**

### 1. Get the code

```bash
git clone https://github.com/akshay-kumar-bm/universal-api-mcp.git
cd universal-api-mcp
npm install --legacy-peer-deps
npx wrangler login
```

> `--legacy-peer-deps` is required. Dependencies are pinned on purpose; keep them pinned.

### 2. Pick your server URL

Your Worker will live at:

```
https://infisical-mcp.<your-subdomain>.workers.dev
```

Find `<your-subdomain>` in the Cloudflare dashboard under **Workers & Pages** (shown on the right, e.g. `jane.workers.dev`). This full URL is your **`PUBLIC_URL`**.

### 3. Create the storage Cloudflare needs

```bash
npx wrangler kv namespace create OAUTH_KV
```

Copy the `id` it prints.

### 4. Set up Infisical (your vault)

1. Create a **project** in Infisical. Copy its **Project ID** (Project settings).
2. Create a **Machine Identity** with **Universal Auth** (Organization → Access Control → Identities).
3. Create a **Client Secret** for it. Copy the **Client ID** and **Client Secret**.
4. Add the identity to your project with a role that can read secrets. Give it create/edit/delete too if you want Claude to manage secrets with `add_secret` etc.
5. Note the environment you'll use (default: `dev`).

### 5. Set up Google sign-in

1. In [Google Cloud Console](https://console.cloud.google.com) → **APIs & Services** → **OAuth consent screen**: choose External, fill in the app name, and add your email as a test user.
2. **Credentials** → **Create credentials** → **OAuth client ID** → type **Web application**.
3. Under **Authorized redirect URIs** add:
   ```
   https://infisical-mcp.<your-subdomain>.workers.dev/callback
   ```
4. Copy the **Client ID** and **Client Secret**.

### 6. Fill in `wrangler.jsonc`

Replace the placeholders:

```jsonc
"kv_namespaces": [{ "binding": "OAUTH_KV", "id": "<id from step 3>" }],
"vars": {
  "INFISICAL_HOST": "https://us.infisical.com",          // or https://eu.infisical.com
  "INFISICAL_PROJECT_ID": "<project id from step 4>",
  "DEFAULT_ENVIRONMENT": "dev",
  "ALLOWED_EMAILS": "you@gmail.com,teammate@gmail.com",  // who may sign in
  "PUBLIC_URL": "https://infisical-mcp.<your-subdomain>.workers.dev"
}
```

### 7. Store the server's own secrets

Each command asks you to paste the value:

```bash
npx wrangler secret put INFISICAL_CLIENT_ID
npx wrangler secret put INFISICAL_CLIENT_SECRET
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
```

### 8. Deploy

```bash
npx wrangler deploy
```

### 9. Check it works

```bash
curl https://infisical-mcp.<your-subdomain>.workers.dev/.well-known/oauth-authorization-server
# 200 with JSON  -> the server is up

curl -i https://infisical-mcp.<your-subdomain>.workers.dev/mcp
# 401 Unauthorized -> correct, you haven't signed in yet
```

> **Deploy from GitHub instead?** `.github/workflows/deploy.yml` runs `wrangler deploy` when you start it by hand (Actions → Deploy → Run workflow). Add repo secrets `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` first. To deploy on every push to `main`, add a `push:` trigger in that file.

---

## Connect it to Claude

Your MCP endpoint is:

```
https://infisical-mcp.<your-subdomain>.workers.dev/mcp
```

### claude.ai and Claude Desktop

1. **Settings** → **Connectors** → **Add custom connector**
2. Name: `Universal API`. URL: your `/mcp` endpoint.
3. Click **Connect** and sign in with a Google account listed in `ALLOWED_EMAILS`.
4. Enable the connector in a chat (the tools menu) and start asking.

### Claude Code

```bash
claude mcp add --transport http universal-api https://infisical-mcp.<your-subdomain>.workers.dev/mcp
```

Then run `/mcp` inside Claude Code and choose **Authenticate** to sign in with Google.

### Other MCP clients

Any client that supports **remote MCP over HTTP with OAuth** works. Point it at the `/mcp` URL; it will discover the sign-in flow automatically.

---

## Add your API tokens

Every token needs two things: a **name** (how Claude refers to it) and **hosts** (where it may be sent).

### Option A: in the Infisical dashboard (recommended)

The token never touches the chat.

1. Open your Infisical project → environment `dev`.
2. Create a **folder per service**, e.g. `/github`, `/notion`, `/linear`.
3. Add the secret inside it, e.g. name `GITHUB_TOKEN`, value `ghp_...`.
4. In the secret's **comment**, add a hosts line:
   ```
   hosts: api.github.com, uploads.github.com
   ```
5. Done. Ask Claude "list my secrets" to confirm it shows `approved_hosts`.

### Option B: ask Claude

> "Store my Linear API key as LINEAR_API_TOKEN in /linear, allowed host api.linear.app. The key is lin_api_..."

Claude calls `add_secret` with the hosts. This is quick, but the value does pass through the chat once, so prefer Option A for important tokens.

### Rules for `hosts:`

- Bare hostnames only: `api.example.com` (no `https://`, path, port or IP).
- Wildcards cover subdomains: `*.atlassian.net` matches `yourteam.atlassian.net`, but never the bare `atlassian.net`.
- Wildcards on shared-hosting domains (`*.workers.dev`, `*.github.io`, `*.vercel.app`, `*.amazonaws.com`, ...) are refused, because anyone can register a subdomain there. Name the exact host instead.
- **Hosts are locked at creation.** Claude can't change them later, even with `update_secret`. Only you can, by editing the comment in Infisical. This stops a tricked model from redirecting a token.
- A secret with no `hosts:` line is **never sent anywhere**. Use that for IDs and config values.

### Common services

| Service | Suggested name | `hosts:` line | How Claude calls it |
|---|---|---|---|
| GitHub | `GITHUB_TOKEN` | `api.github.com, uploads.github.com` | `bearer` (default) |
| Notion | `NOTION_TOKEN` | `api.notion.com` | `bearer` + header `Notion-Version: 2022-06-28` |
| Linear | `LINEAR_API_TOKEN` | `api.linear.app` | `header`, `header_name: Authorization` |
| Stripe | `STRIPE_SECRET_KEY` | `api.stripe.com` | `bearer` |
| Slack | `SLACK_BOT_TOKEN` | `slack.com` | `bearer` |
| OpenAI | `OPENAI_API_KEY` | `api.openai.com` | `bearer` |
| Jira / Confluence | `JIRA_TOKEN` (value `email:api_token`) | `*.atlassian.net` | `basic` |
| Vercel | `VERCEL_TOKEN` | `api.vercel.com` | `bearer` |
| AWS | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | `*.amazonaws.com` is refused, so list exact hosts, e.g. `sts.us-east-1.amazonaws.com, my-bucket.s3.us-east-1.amazonaws.com` | `aws_sigv4` |

> Use the narrowest token you can: read-only scopes, one repo, one workspace. The host lock limits *where* a token goes; the token's own scopes limit *what* it can do.

---

## Using it

Just talk to Claude. Some things to try:

- "What secrets do I have stored?"
- "Show my GitHub notifications from today."
- "Create a Linear issue titled 'Fix login bug' in the Web team."
- "Find the Notion page called Roadmap and add a bullet: Ship v2 in March."
- "How much did we make on Stripe this week?"
- "Who am I on AWS?" (calls STS `GetCallerIdentity`)

Under the hood Claude makes calls like:

```text
api_request { url: "https://api.github.com/user", secret: "GITHUB_TOKEN" }

api_request { url: "https://api.notion.com/v1/users/me", secret: "NOTION_TOKEN",
              headers: { "Notion-Version": "2022-06-28" } }

api_request { url: "https://api.linear.app/graphql", method: "POST", secret: "LINEAR_API_TOKEN",
              auth_type: "header", header_name: "Authorization",
              body: { query: "{ viewer { name } }" } }

api_request { url: "https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15",
              auth_type: "aws_sigv4", aws_service: "sts", aws_region: "us-east-1" }
```

**Uploading a file** (up to 5 MB) works with `multipart` or `body_base64`. For example, attaching an image to a Notion page takes three calls: create a file upload, send the bytes with `multipart`, then reference the upload id on the page. For bigger files, see [`get_upload_url`](#big-files-get_upload_url).

**Secrets in several folders:** `api_request` finds a secret by name in any folder. If the same name exists in two folders, it refuses to guess and asks for `path` (e.g. `path: "/github"`).

---

## Approvals for risky actions

Some actions need a human, and the **server** enforces it, not the model:

- `delete_secret`
- `update_secret` with a new value or a new name
- `api_request` calls that look destructive:
  - method `DELETE`
  - GraphQL mutations whose names contain delete / destroy / remove / archive / purge / trash (e.g. Linear `issueDelete`)
  - JSON bodies that set `archived`, `trashed`, `deleted` or `in_trash` to `true` (how Notion and GitHub archive)
- `issue_temp_token` (always)

**What you see:**

1. Claude says the action needs approval and gives you a link.
2. You open it. The first time, you sign in with Google. That keeps you signed in for 12 hours, so later approvals are one click.
3. The page shows the exact action (URL, method, body, secret). Press **Approve** or **Deny**.
4. Tell Claude you approved. It repeats the identical call, which now runs.

An approval works **once**, only for **that exact action** (same URL, name and body), and expires after **15 minutes**. Claude can't approve for you: it never has your browser session.

Everything else (reads, creates, ordinary updates) runs right away.

---

## Optional extras

### Big files: `get_upload_url`

Move files of up to 5 GB between your machine and other APIs without sending them through the chat. Claude gets a one-time upload URL, runs one `curl -T <file>`, and receives a download link (valid for 1 hour) to pass to another API. Files are deleted automatically after 1 day.

One-time setup with Cloudflare R2:

```bash
npx wrangler r2 bucket create <bucket>
npx wrangler r2 bucket lifecycle add <bucket> --name expire-tmp --prefix tmp/ --expire-days 1
# Cloudflare dashboard → R2 → Manage API tokens → Object Read & Write on that bucket, then:
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
```

Then uncomment `R2_ACCOUNT_ID` (your Cloudflare account ID) and `R2_BUCKET` in `wrangler.jsonc` and deploy again.

### CLI access: `issue_temp_token`

Some tools need credentials in their own environment (`aws`, `terraform`, ...). `issue_temp_token` gives Claude a short-lived Infisical token to use with `infisical run -- <command>`:

- It uses a **separate, limited "agent" identity**, never the server's own.
- Every request shows you the purpose, what the token can read, whether it can write, and its lifetime. You must **tick a box** to give it.
- The server refuses identities whose tokens live longer than 1 hour.

Setup: create a second Machine Identity with a short token TTL (15–60 min) and a read-only role, then:

```bash
npx wrangler secret put AGENT_INFISICAL_CLIENT_ID
npx wrangler secret put AGENT_INFISICAL_CLIENT_SECRET
```

Optional: set `AGENT_PROJECT_ID` to a separate small project so temp tokens can't reach your main vault, and `AGENT_REQUIRE_READ_ONLY` to `"true"` to refuse identities that can write. Full guide: [docs/agent-access.md](docs/agent-access.md), or ask Claude to run `access_guide`.

---

## Security model and limits

**What protects your tokens**

- **Values never come back.** `list_secrets` and `get_secret` never return values. Every response from `api_request` has the token redacted from its body and headers.
- **Host lock.** A token is only sent to the hosts in its own `hosts:` line, which is fixed when the secret is created.
- **No redirects.** `api_request` never follows redirects, since the next hop could be a host the token isn't allowed to reach.
- **No secret-in-secret leaks.** `${OTHER_SECRET}` references are never expanded.
- **No header tricks.** Claude can't set its own `Authorization` header, and method-override headers are refused, so a `DELETE` can't be disguised.
- **Human approval** for deletes, overwrites and temp tokens, as described above.
- **Sign-in allowlist.** Only `ALLOWED_EMAILS` can connect.

**Limits**

- HTTPS only, default port only
- 25-second timeout per request
- Responses capped at 60 KB (the result says when it was truncated)
- Uploads through `api_request` capped at 5 MB (use `get_upload_url` for bigger files)
- Detecting non-`DELETE` destructive calls is best effort (`src/destructive.ts`): it recognises the common wording above, but not every API's.
- This is not a shell. For CLIs use `issue_temp_token` with the Infisical CLI.

---

## Troubleshooting

| Problem | Fix |
|---|---|
| **"Secret X is not allowed to be sent to &lt;host&gt;"** | The secret's `hosts:` line doesn't include that hostname. Edit the comment in Infisical. |
| **"Secret X exists in several folders"** | Two secrets share the name. Pass `path`, or rename one. |
| **Google says `redirect_uri_mismatch`** | The redirect URI in Google Cloud must be exactly `<PUBLIC_URL>/callback`. |
| **"This Google account is not allowed."** | Your email isn't in `ALLOWED_EMAILS`. Add it and redeploy. |
| **`/mcp` returns 401** | Expected before sign-in. Connect through Claude, which runs the sign-in flow. |
| **Infisical errors (401/403)** | Check `INFISICAL_CLIENT_ID` / `INFISICAL_CLIENT_SECRET`, that the identity is added to the project, and that `INFISICAL_HOST` matches your region (US/EU). |
| **`get_upload_url` or `issue_temp_token` only return setup steps** | Their optional setup isn't done yet. See [Optional extras](#optional-extras). |
| **`npm install` fails** | Use `npm install --legacy-peer-deps`. |

---

## Local development

Create `.dev.vars` (git-ignored):

```ini
INFISICAL_CLIENT_ID=...
INFISICAL_CLIENT_SECRET=...
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
PUBLIC_URL=http://localhost:8787
```

Add `http://localhost:8787/callback` as a redirect URI in Google Cloud, then:

```bash
npm run dev         # wrangler dev on http://localhost:8787
npm run typecheck   # run before every push
```

There's no test suite yet. Contributions are welcome.

> Infisical endpoints used: v4 list/get/delete secrets and v2 folders are verified against the Infisical docs. v4 create/update and the environments lookup follow the same shape but weren't checked live.

---

## Project layout

```
src/
  index.ts           OAuth provider (/mcp, /authorize, /token, /register) around the MCP server
  server.ts          MCP server and all tools
  api-call.ts        api_request: auth types, host check, uploads, redaction
  hosts.ts           validation and parsing of the "hosts:" comment line
  destructive.ts     detects destructive requests beyond HTTP DELETE
  approvals.ts       approval records and the /approve pages
  approval-store.ts  Durable Object for approvals (strongly consistent)
  store-core.ts      expiry and sweep logic for the store
  google-handler.ts  Google sign-in, consent page, /callback
  infisical.ts       small Infisical REST client (Universal Auth)
  temp-token.ts      issue_temp_token
  uploads.ts         get_upload_url (R2 presigned URLs)
  guide.ts           access_guide text
docs/agent-access.md full guide for temporary CLI access
wrangler.jsonc       Worker config (fill in the placeholders)
```

---

## License

MIT, see [LICENSE](LICENSE).