Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Smoke tools/list
run: npm run smoke:tools-list
env:
ONE_MSG_BASE_URL: https://api.stage.1msg.io
ONE_MSG_BASE_URL: https://api.1msg.io
ONE_MSG_INSTANCE_ID: TEST_INSTANCE
ONE_MSG_TOKEN: test-token

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- name: Smoke tools/list
run: npm run smoke:tools-list
env:
ONE_MSG_BASE_URL: https://api.stage.1msg.io
ONE_MSG_BASE_URL: https://api.1msg.io
ONE_MSG_INSTANCE_ID: TEST_INSTANCE
ONE_MSG_TOKEN: test-token

Expand Down
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MCP package architecture

The `@1msg/mcp` package **must not** call the Chat API directly.
The `@1msg/mcp` package **must not** call the 1MSG API directly.
All HTTP goes through [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk).

```text
Expand All @@ -15,5 +15,5 @@ Generated files (`src/tools.generated.ts`, `src/handlers.generated.ts`) are prod

| Transport | Entry | Config |
|-----------|-------|--------|
| stdio | `dist/index.js` | Process env `ONE_MSG_*` (deprecated: `CHAT_API_*`) |
| HTTP (hosted) | `dist/http.js` | Per request: `Authorization: Bearer` + `X-Instance-Id`; upstream `ONE_MSG_BASE_URL` fixed on host |
| stdio | `dist/index.js` | Process env `ONE_MSG_*` (deprecated: `CHAT_API_*`). API root: `https://api.1msg.io` or `https://sandbox.1msg.io` |
| HTTP (Cloud) | `dist/http.js` | Per request: `Authorization: Bearer` + `X-Instance-Id`; optional `X-1msg-Base-Url` (`https://api.1msg.io` or `https://sandbox.1msg.io`). Default upstream from host `ONE_MSG_BASE_URL` |
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hosted @1msg/mcp (Streamable HTTP)
# Cloud @1msg/mcp (Streamable HTTP)
# docker build -t 1msg/1msg-mcp-http:local .
# docker run --rm -p 3100:3100 -e ONE_MSG_BASE_URL=https://api.stage.1msg.io 1msg/1msg-mcp-http:local
# docker run --rm -p 3100:3100 -e ONE_MSG_BASE_URL=https://api.1msg.io 1msg/1msg-mcp-http:local

FROM node:20-alpine AS build
WORKDIR /app
Expand Down
45 changes: 30 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
# @1msg/mcp

MCP server for the [1msg](https://1msg.io) Chat API — **60 public tools** for Cursor, Claude Desktop, and other [MCP](https://modelcontextprotocol.io/) clients.
MCP server for the [1MSG](https://1msg.io) WhatsApp Business API — **60 public tools** for Cursor, Claude Desktop, and other [MCP](https://modelcontextprotocol.io/) clients.

Transports: **stdio** (local) and **Streamable HTTP** (hosted).
Transports: **stdio** (local) and **Streamable HTTP** (Cloud).

| | |
|--|--|
| **npm** | [`@1msg/mcp`](https://www.npmjs.com/package/@1msg/mcp) |
| **Hosted prod** | https://mcp.1msg.io/mcp |
| **Hosted stage** | https://mcp.stage.1msg.io/mcp |
| **Cloud** | https://mcp.1msg.io/mcp |
| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) |

API hosts:

| Environment | `ONE_MSG_BASE_URL` |
|-------------|--------------------|
| Live channels | `https://api.1msg.io` |
| Test channels | `https://sandbox.1msg.io` |

> Source of truth for **codegen** of `tools.generated.ts` / `handlers.generated.ts` still lives in the `1msg-api` monorepo (`packages/mcp`). This repository is the **distribution** package for running and hosting the server.

## Quick start

### npx (stdio)

```bash
export ONE_MSG_BASE_URL=https://api.stage.1msg.io
export ONE_MSG_BASE_URL=https://api.1msg.io
export ONE_MSG_INSTANCE_ID=your-instance-id
export ONE_MSG_TOKEN=your-channel-token

Expand All @@ -27,6 +33,8 @@ npx -y @1msg/mcp
# npm start
```

For **test channels**, set `ONE_MSG_BASE_URL=https://sandbox.1msg.io` (bare `sandbox.1msg.io` is also accepted).

Deprecated aliases still work: `CHAT_API_BASE_URL`, `CHAT_API_TOKEN`, `CHAT_API_INSTANCE_ID`.

### Clone and run
Expand All @@ -37,7 +45,7 @@ cd 1msg-mcp
npm install
npm run build

export ONE_MSG_BASE_URL=https://api.stage.1msg.io
export ONE_MSG_BASE_URL=https://api.1msg.io
export ONE_MSG_INSTANCE_ID=your-instance-id
export ONE_MSG_TOKEN=your-channel-token

Expand All @@ -49,27 +57,29 @@ npm run start:http # Streamable HTTP → dist/http.js

| Variable | Required | Description |
|----------|----------|-------------|
| `ONE_MSG_BASE_URL` | yes (stdio) | API base, e.g. `https://api.stage.1msg.io` or `https://api.1msg.io` |
| `ONE_MSG_BASE_URL` | yes (stdio) | API root: `https://api.1msg.io` (live) or `https://sandbox.1msg.io` (test) |
| `ONE_MSG_TOKEN` | yes (stdio) | Channel API token |
| `ONE_MSG_INSTANCE_ID` | yes (stdio) | Channel instance id |
| `MCP_HTTP_HOST` | no | HTTP bind host (default `0.0.0.0` in Docker) |
| `MCP_HTTP_PORT` | no | HTTP port (default `3100`) |
| `MCP_RATE_LIMIT_RPM` | no | Per-token rate limit for HTTP (default `60`) |

For **hosted HTTP**, upstream `ONE_MSG_BASE_URL` is set on the server; clients send credentials per request (see below).
For **Cloud HTTP**, the server default upstream is `ONE_MSG_BASE_URL` (typically live). Clients send credentials per request; test channels can also send `X-1msg-Base-Url: https://sandbox.1msg.io`.

## Cursor (`mcp.json`)

### Local stdio

Live channel:

```json
{
"mcpServers": {
"1msg": {
"command": "npx",
"args": ["-y", "@1msg/mcp"],
"env": {
"ONE_MSG_BASE_URL": "https://api.stage.1msg.io",
"ONE_MSG_BASE_URL": "https://api.1msg.io",
"ONE_MSG_INSTANCE_ID": "your-instance-id",
"ONE_MSG_TOKEN": "your-channel-token"
}
Expand All @@ -78,9 +88,11 @@ For **hosted HTTP**, upstream `ONE_MSG_BASE_URL` is set on the server; clients s
}
```

### Hosted URL (Streamable HTTP)
Test channel: set `"ONE_MSG_BASE_URL": "https://sandbox.1msg.io"`.

### Cloud URL (Streamable HTTP)

Prod: [https://mcp.1msg.io/mcp](https://mcp.1msg.io/mcp) · Stage: [https://mcp.stage.1msg.io/mcp](https://mcp.stage.1msg.io/mcp)
Cloud endpoint: [https://mcp.1msg.io/mcp](https://mcp.1msg.io/mcp)

```json
{
Expand All @@ -96,23 +108,26 @@ Prod: [https://mcp.1msg.io/mcp](https://mcp.1msg.io/mcp) · Stage: [https://mcp.
}
```

For a **test channel**, add `"X-1msg-Base-Url": "https://sandbox.1msg.io"`.

Header aliases: `X-1msg-Instance-Id` (and deprecated `X-Chat-Api-Instance-Id`). Do not commit real tokens.

## Hosted HTTP locally
## Cloud HTTP locally

```bash
export ONE_MSG_BASE_URL=https://api.stage.1msg.io
export ONE_MSG_BASE_URL=https://api.1msg.io
export MCP_HTTP_PORT=3100
npm run start:http
```

- Auth: `Authorization: Bearer <token>` + `X-Instance-Id: <instanceId>`
- Optional: `X-1msg-Base-Url: https://sandbox.1msg.io` for test channels
- Health: `GET /healthz`, `GET /readyz`

```bash
docker build -t 1msg/1msg-mcp-http:local .
docker run --rm -p 3100:3100 \
-e ONE_MSG_BASE_URL=https://api.stage.1msg.io \
-e ONE_MSG_BASE_URL=https://api.1msg.io \
1msg/1msg-mcp-http:local
```

Expand All @@ -127,7 +142,7 @@ docker run --rm -p 3100:3100 \

## Architecture

All Chat API HTTP goes through `@1msg/sdk`. See [ARCHITECTURE.md](./ARCHITECTURE.md).
All API HTTP goes through `@1msg/sdk`. See [ARCHITECTURE.md](./ARCHITECTURE.md).

## License

Expand Down
2 changes: 1 addition & 1 deletion scripts/smoke-tools-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function main() {
const serverEntry = path.resolve(__dirname, '../dist/index.js');
const env = {
...process.env,
ONE_MSG_BASE_URL: process.env.ONE_MSG_BASE_URL || process.env.CHAT_API_BASE_URL || 'https://api.stage.1msg.io',
ONE_MSG_BASE_URL: process.env.ONE_MSG_BASE_URL || process.env.CHAT_API_BASE_URL || 'https://api.1msg.io',
ONE_MSG_INSTANCE_ID: process.env.ONE_MSG_INSTANCE_ID || process.env.CHAT_API_INSTANCE_ID || 'TEST_INSTANCE',
ONE_MSG_TOKEN: process.env.ONE_MSG_TOKEN || process.env.CHAT_API_TOKEN || 'test-token',
};
Expand Down
29 changes: 27 additions & 2 deletions src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,41 @@ describe('MCP HTTP auth', () => {

it('builds request config from host + headers', () => {
const config = resolveRequestConfig(
{ baseUrl: 'https://api.stage.1msg.io' },
{ baseUrl: 'https://api.1msg.io' },
headers({
authorization: 'Bearer tok',
'x-instance-id': 'INST',
}),
);
expect(config).toEqual({
baseUrl: 'https://api.stage.1msg.io',
baseUrl: 'https://api.1msg.io',
token: 'tok',
instanceId: 'INST',
});
});

it('allows sandbox.1msg.io via X-1msg-Base-Url', () => {
const config = resolveRequestConfig(
{ baseUrl: 'https://api.1msg.io' },
headers({
authorization: 'Bearer tok',
'x-instance-id': 'HEI123',
'x-1msg-base-url': 'http://sandbox.1msg.io',
}),
);
expect(config.baseUrl).toBe('https://sandbox.1msg.io');
});

it('rejects non-public X-1msg-Base-Url overrides', () => {
expect(() =>
resolveRequestConfig(
{ baseUrl: 'https://api.1msg.io' },
headers({
authorization: 'Bearer tok',
'x-instance-id': 'INST',
'x-1msg-base-url': 'https://evil.example',
}),
),
).toThrow(/Unsupported X-1msg-Base-Url/);
});
});
49 changes: 40 additions & 9 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
import { loadMcpConfig } from '../config';
import { loadMcpConfig, normalizeBaseUrl } from '../config';

describe('normalizeBaseUrl', () => {
it('accepts live and sandbox hosts with or without scheme', () => {
expect(normalizeBaseUrl('https://api.1msg.io')).toBe('https://api.1msg.io');
expect(normalizeBaseUrl('https://api.1msg.io/')).toBe('https://api.1msg.io');
expect(normalizeBaseUrl('sandbox.1msg.io')).toBe('https://sandbox.1msg.io');
expect(normalizeBaseUrl('http://sandbox.1msg.io')).toBe(
'https://sandbox.1msg.io',
);
expect(normalizeBaseUrl('https://sandbox.1msg.io/HEI123/status')).toBe(
'https://sandbox.1msg.io',
);
expect(normalizeBaseUrl('https://api.sandbox.1msg.io')).toBe(
'https://sandbox.1msg.io',
);
});

it('keeps custom hosts (origin only)', () => {
expect(normalizeBaseUrl('https://api.example.test:8443/v1')).toBe(
'https://api.example.test:8443',
);
});
});

describe('loadMcpConfig', () => {
const originalEnv = process.env;
Expand All @@ -19,24 +42,32 @@ describe('loadMcpConfig', () => {
});

it('loads config from ONE_MSG_* env vars', () => {
process.env.ONE_MSG_BASE_URL = 'https://api.stage.1msg.io';
process.env.ONE_MSG_BASE_URL = 'https://api.1msg.io';
process.env.ONE_MSG_TOKEN = 'secret-token';
process.env.ONE_MSG_INSTANCE_ID = 'ODI371267300';

expect(loadMcpConfig()).toEqual({
baseUrl: 'https://api.stage.1msg.io',
baseUrl: 'https://api.1msg.io',
token: 'secret-token',
instanceId: 'ODI371267300',
});
});

it('normalizes sandbox.1msg.io for test channels', () => {
process.env.ONE_MSG_BASE_URL = 'http://sandbox.1msg.io';
process.env.ONE_MSG_TOKEN = 'secret-token';
process.env.ONE_MSG_INSTANCE_ID = 'HEI123';

expect(loadMcpConfig().baseUrl).toBe('https://sandbox.1msg.io');
});

it('falls back to deprecated CHAT_API_* env vars', () => {
process.env.CHAT_API_BASE_URL = 'https://api.stage.1msg.io';
process.env.CHAT_API_BASE_URL = 'https://sandbox.1msg.io';
process.env.CHAT_API_TOKEN = 'secret-token';
process.env.CHAT_API_INSTANCE_ID = 'ODI371267300';

expect(loadMcpConfig()).toEqual({
baseUrl: 'https://api.stage.1msg.io',
baseUrl: 'https://sandbox.1msg.io',
token: 'secret-token',
instanceId: 'ODI371267300',
});
Expand All @@ -46,7 +77,7 @@ describe('loadMcpConfig', () => {
process.env.ONE_MSG_BASE_URL = 'https://api.1msg.io';
process.env.ONE_MSG_TOKEN = 'new-token';
process.env.ONE_MSG_INSTANCE_ID = 'NEW123';
process.env.CHAT_API_BASE_URL = 'https://api.stage.1msg.io';
process.env.CHAT_API_BASE_URL = 'https://sandbox.1msg.io';
process.env.CHAT_API_TOKEN = 'old-token';
process.env.CHAT_API_INSTANCE_ID = 'OLD123';

Expand All @@ -58,22 +89,22 @@ describe('loadMcpConfig', () => {
});

it('accepts INSTANCE_ID alias for instance id', () => {
process.env.ONE_MSG_BASE_URL = 'https://api.stage.1msg.io';
process.env.ONE_MSG_BASE_URL = 'https://api.1msg.io';
process.env.ONE_MSG_TOKEN = 'secret-token';
process.env.INSTANCE_ID = 'ODI371267300';

expect(loadMcpConfig().instanceId).toBe('ODI371267300');
});

it('throws with actionable message when token is missing', () => {
process.env.ONE_MSG_BASE_URL = 'https://api.stage.1msg.io';
process.env.ONE_MSG_BASE_URL = 'https://api.1msg.io';
process.env.ONE_MSG_INSTANCE_ID = 'ODI371267300';

expect(() => loadMcpConfig()).toThrow(/ONE_MSG_TOKEN/);
});

it('throws when instance id is missing', () => {
process.env.ONE_MSG_BASE_URL = 'https://api.stage.1msg.io';
process.env.ONE_MSG_BASE_URL = 'https://api.1msg.io';
process.env.ONE_MSG_TOKEN = 'secret-token';

expect(() => loadMcpConfig()).toThrow(/INSTANCE_ID/);
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/mock-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface MockChatApiClientBundle extends MockChatApiClientMocks {
/** Jest mock ChatApiClient with call tracking for MCP integration tests. */
export function createMockChatApiClient(): MockChatApiClientBundle {
const config = new ChatApiConfig({
baseUrl: 'https://api.stage.1msg.io',
baseUrl: 'https://api.1msg.io',
instanceId: 'TEST_INSTANCE',
token: 'test-token',
});
Expand Down
Loading
Loading