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
58 changes: 58 additions & 0 deletions .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Owners and constraints for this repository's actual runtime roots.
module.exports = {
forbidden: [
{
name: 'no-runtime-cycles',
severity: 'error',
from: {},
to: {
circular: true,
dependencyTypesNot: ['type-only'],
},
},
{
name: 'no-unresolved-dependencies',
severity: 'error',
from: {},
to: {
couldNotResolve: true,
},
},
{
name: 'no-runtime-test-dependencies',
severity: 'error',
from: {
path: '^(?:src)/',
pathNot: '(?:__tests__|test|tests)/|\\.(?:test|spec)\\.',
},
to: {
path: '(?:^|/)(?:__tests__|tests|test)/|(?:^|node_modules/)(?:jest|vitest|ts-jest)',
},
},
{
name: 'no-sdk-in-pure-transforms',
severity: 'error',
from: {
path: '^src/(?:auth|json-schema-to-zod|logging|rate-limit)\\.(?:ts|tsx|mts|cts)$',
},
to: {
path: 'node_modules/(?:@1msg|express|@modelcontextprotocol)/',
},
},
],
options: {
doNotFollow: {
path: 'node_modules',
},
tsConfig: {
fileName: 'tsconfig.quality.json',
},
tsPreCompilationDeps: true,
enhancedResolveOptions: {
exportsFields: ['exports'],
conditionNames: ['require', 'node', 'default'],
extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.json'],
},
exclude: '(?:^|/)node_modules/',
},
};
79 changes: 79 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Quality
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: quality-${{ github.ref }}
cancel-in-progress: true
jobs:
checks:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '24.19.0'
- run: npm ci
- run: npm --prefix tools/quality ci
- run: npm run test:preset
- run: npm run quality:guards
- run: npm run format:check
- run: npm run lint:static
- run: npm run lint:typed
- run: npm run typecheck
- run: npm run lint:arch
- run: npm run lint:unused:production
- run: npm run lint:unused:full
- run: npm run lint:duplicates
- run: npm run lint:duplicates:full
- run: npm run lint:complexity
- run: npm run test:generated
- run: npm run test:quality
- run: npm test
- run: npm run build
- run: npm run test:pack
- uses: actions/upload-artifact@v4
if: always()
with:
name: quality-reports
path: quality/reports/
if-no-files-found: error
package-consumer:
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node: ['18', '20', '22', '24']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24.19.0'
- run: npm ci --ignore-scripts
- run: npm run build
- name: Pack the actual distributable
run: mkdir -p "$RUNNER_TEMP/packed" && npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/packed"
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: bash scripts/consumer-smoke.sh "$RUNNER_TEMP"/packed/*.tgz '@1msg/mcp'
release-artifact:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: bash scripts/container-smoke.sh
quality-required:
if: always()
needs: [checks, package-consumer, release-artifact]
runs-on: ubuntu-24.04
steps:
- env:
RESULTS: ${{ toJSON(needs) }}
run: node -e 'const jobs=JSON.parse(process.env.RESULTS); if(!Object.values(jobs).every(job=>job.result==="success")) process.exit(1)'
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ coverage/
!.env.example
npm-debug.log*
node_modules/

quality/reports/

.husky/_/

# Local shared-tool dependency links are never repository content.
tools/quality/node_modules
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node tools/quality/node_modules/lint-staged/bin/lint-staged.js
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.19.0
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
tools/quality
quality/reports
src/tools.generated.ts
src/handlers.generated.ts

quality/mcp/vendor/
quality/mcp/openapi.yaml
5 changes: 5 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Quality and ownership

Profile A, Node 24.19.0. SDK/tooling maintainers own this repository. The public package entrypoints are defined in package.json; framework registration and executable consumers must remain intact. Imports must be acyclic at runtime, production cannot import tests, and dependency boundaries are enforced in `.dependency-cruiser.cjs`. `quality.config.mjs` is the source inventory and exact generated-path declaration.

Configuration belongs in the existing config/bootstrap adapters; pure conversion/validation helpers must not gain transport or environment reads. SDK and HTTP calls belong in the existing transport/client adapters. Keep complete scenarios readable at their application entrypoints; avoid pass-through facade layers. Comments on new/changed functions describe purpose, inputs/results, invariants and relevant failure/resource ownership. Check comments against behavior rather than repeating the function name.

Install with the frozen lockfile, then `npm --prefix tools/quality ci`. Run `npm run hooks:install` for local Husky/lint-staged feedback. Run all commands listed in `.github/workflows/quality.yml` before review: formatter, static and typed ESLint, strict production/test typecheck, dependency-cruiser, both Knip graphs, jscpd runtime gate and full report, complexity, guard negative fixtures, tests, build and packed-entrypoint smoke. No CI skip variable disables typed lint.

Historical debt is recorded per finding in `quality/baselines`, with adoption SHA, evidence, owner, issue and expiry. Initial allowances are replayed against the default branch; new findings and increases are forbidden. Remove entries when fixed. Generated paths have producer provenance; authored tooling is always checked.

The `quality-required` job fails on failed/skipped/cancelled prerequisites. Configure branch protection to require it; YAML alone does not change server protection. Tests use isolated/local inputs; never point them at production services.

Generated MCP files are owned by the pinned API producer. Run `npm run test:generated` to replay the exact producer against the checksummed public contract and locked SDK declarations, including changed/deleted/ignored-output negative fixtures. Update the producer provenance before deliberately running `node quality/mcp/generate.mjs --write`; never hand-edit the two generated files.

The strict source program excludes only `quality/mcp/vendor/apis/**`: these incomplete API snapshots are checksummed generator inputs, not executable modules. `test:generated` verifies their exact provenance; generated runtime outputs remain in the strict program. All authored TS/TSX/MTS/CTS files remain covered.

Jest uses the strict quality TypeScript program with isolated module compilation for NodeNext. Tests must release their listeners/transports and terminate naturally; forced process exit is not a substitute for cleanup.
8 changes: 4 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ OpenAPI YAML (1msg-api monorepo)

Generated files (`src/tools.generated.ts`, `src/handlers.generated.ts`) are produced in `1msg-api` and synced into this distribution repo when tools change.

| Transport | Entry | Config |
|-----------|-------|--------|
| 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` |
| Transport | Entry | Config |
| ------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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` |
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
# docker build -t 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
FROM node:24.19.0-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci || npm install
RUN npm ci
COPY tsconfig.json tsconfig.build.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev

FROM node:20-alpine
FROM node:24.19.0-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV MCP_HTTP_HOST=0.0.0.0
Expand Down
45 changes: 22 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ MCP server for the [1MSG](https://1msg.io) WhatsApp Business API — **62 public

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

| | |
|--|--|
| **npm** | [`@1msg/mcp`](https://www.npmjs.com/package/@1msg/mcp) |
| **Cloud** | https://mcp.1msg.io/mcp |
| **SDK** | [`@1msg/sdk`](https://www.npmjs.com/package/@1msg/sdk) |
| | |
| --------- | ------------------------------------------------------ |
| **npm** | [`@1msg/mcp`](https://www.npmjs.com/package/@1msg/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` |
| 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.
Expand All @@ -35,7 +35,6 @@ npx -y @1msg/mcp

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


### Clone and run

```bash
Expand All @@ -54,14 +53,14 @@ npm run start:http # Streamable HTTP → dist/http.js

## Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `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`) |
| Variable | Required | Description |
| --------------------- | ----------- | -------------------------------------------------------------------------- |
| `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 **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`.

Expand Down Expand Up @@ -132,12 +131,12 @@ docker run --rm -p 3100:3100 \

## Scripts

| Script | Purpose |
|--------|---------|
| `npm run build` | Compile TypeScript → `dist/` |
| `npm test` | Jest unit tests |
| `npm run smoke:tools-list` | Stdio `tools/list` smoke (expects 62 tools; uses dummy token) |
| `npm start` / `npm run start:http` | Run stdio / HTTP servers |
| Script | Purpose |
| ---------------------------------- | ------------------------------------------------------------- |
| `npm run build` | Compile TypeScript → `dist/` |
| `npm test` | Jest unit tests |
| `npm run smoke:tools-list` | Stdio `tools/list` smoke (expects 62 tools; uses dummy token) |
| `npm start` / `npm run start:http` | Run stdio / HTTP servers |

## Architecture

Expand Down
22 changes: 22 additions & 0 deletions eslint.complexity.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { maintainabilityConfig, tseslint } from './tools/quality/eslint.mjs';
export default [
{ files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
{ linterOptions: { reportUnusedDisableDirectives: 'off' } },
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/tools/quality/**',
'quality/mcp/vendor/**',
'**/quality/reports/**',
'src/tools.generated.ts',
'src/handlers.generated.ts',
],
},
{
files: ['**/*.{ts,tsx,cts,mts}'],
languageOptions: { parser: tseslint.parser },
plugins: { '@typescript-eslint': tseslint.plugin },
},
...maintainabilityConfig(),
];
28 changes: 28 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { staticConfig, globals } from './tools/quality/eslint.mjs';

export default [
{ files: ['**/*.jsx'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/tools/quality/**',
'quality/mcp/vendor/**',
'**/quality/reports/**',
'src/tools.generated.ts',
'src/handlers.generated.ts',
],
},
...staticConfig({ globals: { ...globals.node, ...globals.jest } }),
{
settings: {
'import/resolver': {
typescript: {
project: './tsconfig.quality.json',
conditionNames: ['require', 'node', 'default'],
},
node: { extensions: ['.js', '.json', '.ts', '.mjs', '.cjs'] },
},
},
},
];
15 changes: 15 additions & 0 deletions eslint.typed.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { typedConfig } from './tools/quality/eslint.mjs';
export default [
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/tools/quality/**',
'**/quality/reports/**',
'quality/mcp/vendor/apis/**',
'src/tools.generated.ts',
'src/handlers.generated.ts',
],
},
...typedConfig({ project: './tsconfig.quality.json', tsconfigRootDir: import.meta.dirname }),
];
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.quality.json' }] },
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
clearMocks: true,
testTimeout: 15000,
// zod@4 + parallel workers can flake on module resolution; keep CI deterministic
maxWorkers: 1,
forceExit: true,
};
22 changes: 22 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"entry": [
"src/index.ts!",
"src/http.ts!",
"scripts/pack-smoke.mjs",
"scripts/quality-extensions.test.mjs",
"scripts/quality-hook.test.mjs",
"scripts/quality-negative.mjs",
"scripts/smoke-tools-list.js",
"jest.config.js",
"eslint*.mjs",
"quality.config.mjs",
"quality/mcp/generate.mjs!",
"quality/mcp/verify-negative.mjs"
],
"project": [
"src/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}!",
"scripts/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
"quality/mcp/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}!"
],
"ignore": ["tools/quality/**"]
}
Loading
Loading