@ararahq/mcp 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/build/auth/context.js +4 -0
- package/build/auth/device-flow.js +75 -0
- package/build/auth/token-store.js +28 -0
- package/build/auth/tokens.js +39 -0
- package/build/cli.js +71 -0
- package/build/config.js +17 -0
- package/build/index.js +23 -0
- package/build/lib/api.js +45 -0
- package/build/lib/errors.js +49 -0
- package/build/lib/schemas.js +119 -0
- package/build/mcp/result.js +43 -0
- package/build/prompts/index.js +9 -0
- package/build/resources/index.js +60 -0
- package/build/server/create.js +12 -0
- package/build/tools/index.js +284 -0
- package/build/transports/http.js +140 -0
- package/build/transports/stdio.js +6 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Arara Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# AraraHQ MCP
|
|
2
|
+
|
|
3
|
+
Official Model Context Protocol server for operating AraraHQ Atendimento across support, billing and scheduling.
|
|
4
|
+
|
|
5
|
+
Version 5 is a clean break from the legacy CPaaS-oriented server. Node.js is the only canonical implementation, OAuth is the authentication boundary, and both the npm scope and public repository are owned by AraraHQ: [`@ararahq/mcp`](https://www.npmjs.com/package/@ararahq/mcp) and [`ararahq/mcp`](https://github.com/ararahq/mcp).
|
|
6
|
+
|
|
7
|
+
## What it exposes
|
|
8
|
+
|
|
9
|
+
- Atendimento: today queue, paginated conversation discovery and timeline, claim, reply and close.
|
|
10
|
+
- Automations: list and inspect configured automations.
|
|
11
|
+
- Campaigns: preflight and idempotent publication tied to a published Atendimento routine.
|
|
12
|
+
- WhatsApp operations: single-message send, delivery lookup, templates, contacts and opt-outs.
|
|
13
|
+
- Context: organization, operational health, approved templates, channels and coverage.
|
|
14
|
+
|
|
15
|
+
Every tool returns both human-readable content and stable structured content. Write tools carry MCP safety annotations. Credentials never appear in tool inputs or output.
|
|
16
|
+
|
|
17
|
+
## Local installation
|
|
18
|
+
|
|
19
|
+
Requires Node.js 20 or newer.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y @ararahq/mcp login
|
|
23
|
+
npx -y @ararahq/mcp status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The device authorization flow opens AraraHQ in the browser. Access and refresh tokens are stored in the operating-system keychain, never in a project file or client configuration.
|
|
27
|
+
|
|
28
|
+
Configure an MCP client with stdio:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"ararahq": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "@ararahq/mcp", "--stdio"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Useful diagnostics:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx -y @ararahq/mcp doctor
|
|
45
|
+
npx -y @ararahq/mcp tools
|
|
46
|
+
npx -y @ararahq/mcp logout
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Hosted transport
|
|
50
|
+
|
|
51
|
+
The hosted server uses stateless MCP Streamable HTTP at `POST /mcp`. It accepts OAuth bearer tokens only in the `Authorization` header. Query-string credentials, API-key tool arguments, legacy SSE endpoints, permissive CORS and debug endpoints do not exist.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
MCP_TRANSPORT=http \
|
|
55
|
+
PORT=3333 \
|
|
56
|
+
MCP_PUBLIC_URL=https://mcp.ararahq.com/mcp \
|
|
57
|
+
MCP_ALLOWED_HOSTS=mcp.ararahq.com \
|
|
58
|
+
MCP_ALLOWED_ORIGINS=https://chatgpt.com,https://claude.ai \
|
|
59
|
+
ARARA_OAUTH_ISSUER=https://api.ararahq.com/api \
|
|
60
|
+
npm start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Protected Resource Metadata is served at:
|
|
64
|
+
|
|
65
|
+
- `/.well-known/oauth-protected-resource`
|
|
66
|
+
- `/.well-known/oauth-protected-resource/mcp`
|
|
67
|
+
|
|
68
|
+
AraraHQ currently issues installed-client OAuth tokens through its device authorization flow. Hosted clients must supply a valid AraraHQ OAuth bearer token; the MCP does not proxy credentials or mint tokens.
|
|
69
|
+
|
|
70
|
+
## Tool catalog
|
|
71
|
+
|
|
72
|
+
`whoami`, `get_today`, `find_conversations`, `get_conversation`, `reply_to_conversation`, `claim_conversation`, `close_conversation`, `list_automations`, `get_automation`, `prepare_campaign`, `publish_campaign`, `send_whatsapp`, `check_message`, `save_contacts`, `create_template`, `get_template_status`, `opt_out`.
|
|
73
|
+
|
|
74
|
+
Campaign publication validates that the Meta template is approved and available, and that its destination routine (`support`, `billing` or `scheduling`) is published. Message acceptance is reported as queued—not delivered—and delivery is checked separately.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm ci
|
|
80
|
+
npm run check
|
|
81
|
+
npm audit --omit=dev
|
|
82
|
+
npm pack --dry-run
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The server defaults to stdio. Use `MCP_TRANSPORT=http npm start` for Streamable HTTP. Override the API only for controlled environments with `ARARA_API_URL`.
|
|
86
|
+
|
|
87
|
+
The former unscoped package `ararahq-mcp` is the frozen v4 distribution. New installations and every v5 release use `@ararahq/mcp`.
|
|
88
|
+
|
|
89
|
+
## Security
|
|
90
|
+
|
|
91
|
+
- OAuth tokens remain server-side and are redacted by design.
|
|
92
|
+
- External HTTP requests have explicit timeouts.
|
|
93
|
+
- Retries are limited to safe methods or requests carrying an idempotency key and honor `Retry-After`.
|
|
94
|
+
- All consumed API payloads are validated before fields are used.
|
|
95
|
+
- Hosted requests are rate-limited and checked against explicit host and origin allowlists.
|
|
96
|
+
- No telemetry is collected by this package.
|
|
97
|
+
|
|
98
|
+
Report vulnerabilities privately to `security@ararahq.com`.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT © AraraHQ
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { API_TIMEOUT_MS, getApiBaseUrl, OAUTH_CLIENT_ID, OAUTH_SCOPE } from "../config.js";
|
|
5
|
+
import { saveToken } from "./token-store.js";
|
|
6
|
+
const deviceCodeSchema = z.object({
|
|
7
|
+
deviceCode: z.string(),
|
|
8
|
+
userCode: z.string(),
|
|
9
|
+
verificationUri: z.string().url(),
|
|
10
|
+
verificationUriComplete: z.string().url().optional(),
|
|
11
|
+
expiresIn: z.number().positive(),
|
|
12
|
+
interval: z.number().positive(),
|
|
13
|
+
});
|
|
14
|
+
const tokenSchema = z.object({
|
|
15
|
+
accessToken: z.string(),
|
|
16
|
+
refreshToken: z.string().optional(),
|
|
17
|
+
expiresIn: z.number().positive().optional(),
|
|
18
|
+
});
|
|
19
|
+
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
20
|
+
export const requestDeviceCode = async () => {
|
|
21
|
+
const response = await axios.post(`${getApiBaseUrl()}/oauth/device/code`, { clientId: OAUTH_CLIENT_ID, scope: OAUTH_SCOPE }, { timeout: API_TIMEOUT_MS });
|
|
22
|
+
return deviceCodeSchema.parse(response.data);
|
|
23
|
+
};
|
|
24
|
+
export const pollForToken = async (code) => {
|
|
25
|
+
const deadline = Date.now() + code.expiresIn * 1_000;
|
|
26
|
+
let interval = Math.max(2_000, code.interval * 1_000);
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
await wait(interval);
|
|
29
|
+
try {
|
|
30
|
+
const response = await axios.post(`${getApiBaseUrl()}/oauth/device/token`, { clientId: OAUTH_CLIENT_ID, deviceCode: code.deviceCode }, { timeout: API_TIMEOUT_MS });
|
|
31
|
+
const token = tokenSchema.parse(response.data);
|
|
32
|
+
await saveToken({
|
|
33
|
+
accessToken: token.accessToken,
|
|
34
|
+
...(token.refreshToken === undefined ? {} : { refreshToken: token.refreshToken }),
|
|
35
|
+
...(token.expiresIn === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: { expiresAt: Date.now() + token.expiresIn * 1_000 }),
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!axios.isAxiosError(error))
|
|
43
|
+
throw error;
|
|
44
|
+
const payload = error.response?.data;
|
|
45
|
+
const record = typeof payload === "object" && payload !== null ? payload : {};
|
|
46
|
+
const nested = typeof record.error === "object" && record.error !== null
|
|
47
|
+
? record.error
|
|
48
|
+
: record;
|
|
49
|
+
const codeValue = typeof nested.code === "string"
|
|
50
|
+
? nested.code
|
|
51
|
+
: typeof record.error === "string"
|
|
52
|
+
? record.error
|
|
53
|
+
: "";
|
|
54
|
+
if (codeValue === "authorization_pending")
|
|
55
|
+
continue;
|
|
56
|
+
if (codeValue === "slow_down") {
|
|
57
|
+
interval += 2_000;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (codeValue === "access_denied")
|
|
61
|
+
throw new Error("OAuth login was denied.");
|
|
62
|
+
if (codeValue === "expired_token")
|
|
63
|
+
throw new Error("OAuth login expired. Run login again.");
|
|
64
|
+
throw new Error("OAuth token polling failed.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error("OAuth login timed out. Run login again.");
|
|
68
|
+
};
|
|
69
|
+
export const openBrowser = (url) => {
|
|
70
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
71
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
72
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
73
|
+
child.on("error", () => undefined);
|
|
74
|
+
child.unref();
|
|
75
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import keytar from "keytar";
|
|
2
|
+
const SERVICE = "ararahq-mcp";
|
|
3
|
+
const ACCESS = "oauth-access-token";
|
|
4
|
+
const REFRESH = "oauth-refresh-token";
|
|
5
|
+
const EXPIRES = "oauth-expires-at";
|
|
6
|
+
export const saveToken = async (token) => {
|
|
7
|
+
await keytar.setPassword(SERVICE, ACCESS, token.accessToken);
|
|
8
|
+
if (token.refreshToken)
|
|
9
|
+
await keytar.setPassword(SERVICE, REFRESH, token.refreshToken);
|
|
10
|
+
if (token.expiresAt !== undefined)
|
|
11
|
+
await keytar.setPassword(SERVICE, EXPIRES, String(token.expiresAt));
|
|
12
|
+
};
|
|
13
|
+
export const loadToken = async () => {
|
|
14
|
+
const accessToken = await keytar.getPassword(SERVICE, ACCESS);
|
|
15
|
+
if (!accessToken)
|
|
16
|
+
return null;
|
|
17
|
+
const refreshToken = await keytar.getPassword(SERVICE, REFRESH);
|
|
18
|
+
const expiresRaw = await keytar.getPassword(SERVICE, EXPIRES);
|
|
19
|
+
const expiresAt = expiresRaw === null ? undefined : Number(expiresRaw);
|
|
20
|
+
return {
|
|
21
|
+
accessToken,
|
|
22
|
+
...(refreshToken === null ? {} : { refreshToken }),
|
|
23
|
+
...(expiresAt === undefined || !Number.isFinite(expiresAt) ? {} : { expiresAt }),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export const clearToken = async () => {
|
|
27
|
+
await Promise.all([ACCESS, REFRESH, EXPIRES].map((account) => keytar.deletePassword(SERVICE, account)));
|
|
28
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { getApiBaseUrl, API_TIMEOUT_MS, OAUTH_CLIENT_ID } from "../config.js";
|
|
3
|
+
import { getRequestAccessToken } from "./context.js";
|
|
4
|
+
import { loadToken, saveToken } from "./token-store.js";
|
|
5
|
+
const isRefreshResponse = (value) => {
|
|
6
|
+
if (typeof value !== "object" || value === null)
|
|
7
|
+
return false;
|
|
8
|
+
const record = value;
|
|
9
|
+
return (typeof record.accessToken === "string" &&
|
|
10
|
+
(record.refreshToken === undefined || typeof record.refreshToken === "string") &&
|
|
11
|
+
(record.expiresIn === undefined || typeof record.expiresIn === "number"));
|
|
12
|
+
};
|
|
13
|
+
export const getAccessToken = async () => {
|
|
14
|
+
const contextual = getRequestAccessToken();
|
|
15
|
+
if (contextual)
|
|
16
|
+
return contextual;
|
|
17
|
+
const stored = await loadToken();
|
|
18
|
+
if (!stored)
|
|
19
|
+
throw new Error("OAuth login required. Run `ararahq-mcp login`.");
|
|
20
|
+
if (stored.expiresAt === undefined || stored.expiresAt > Date.now() + 30_000)
|
|
21
|
+
return stored.accessToken;
|
|
22
|
+
if (!stored.refreshToken)
|
|
23
|
+
throw new Error("OAuth session expired. Run `ararahq-mcp login` again.");
|
|
24
|
+
const response = await axios.post(`${getApiBaseUrl()}/oauth/token/refresh`, {
|
|
25
|
+
clientId: OAUTH_CLIENT_ID,
|
|
26
|
+
refreshToken: stored.refreshToken,
|
|
27
|
+
}, { timeout: API_TIMEOUT_MS });
|
|
28
|
+
if (!isRefreshResponse(response.data))
|
|
29
|
+
throw new Error("OAuth server returned an invalid refresh response.");
|
|
30
|
+
const refreshed = response.data;
|
|
31
|
+
await saveToken({
|
|
32
|
+
accessToken: refreshed.accessToken,
|
|
33
|
+
refreshToken: refreshed.refreshToken ?? stored.refreshToken,
|
|
34
|
+
...(refreshed.expiresIn === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { expiresAt: Date.now() + refreshed.expiresIn * 1_000 }),
|
|
37
|
+
});
|
|
38
|
+
return refreshed.accessToken;
|
|
39
|
+
};
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_TIMEOUT_MS, getApiBaseUrl, SERVER_VERSION } from "./config.js";
|
|
3
|
+
import { requestDeviceCode, pollForToken, openBrowser } from "./auth/device-flow.js";
|
|
4
|
+
import { clearToken, loadToken } from "./auth/token-store.js";
|
|
5
|
+
import { identitySchema } from "./lib/schemas.js";
|
|
6
|
+
import { TOOL_NAMES } from "./tools/index.js";
|
|
7
|
+
const log = (message) => {
|
|
8
|
+
process.stdout.write(`${message}\n`);
|
|
9
|
+
};
|
|
10
|
+
const login = async () => {
|
|
11
|
+
const code = await requestDeviceCode();
|
|
12
|
+
const url = code.verificationUriComplete ??
|
|
13
|
+
`${code.verificationUri}?code=${encodeURIComponent(code.userCode)}`;
|
|
14
|
+
log(`Authorize AraraHQ MCP at:\n${url}\nCode: ${code.userCode}`);
|
|
15
|
+
openBrowser(url);
|
|
16
|
+
await pollForToken(code);
|
|
17
|
+
log("OAuth login complete. Tokens are stored in the operating-system keychain.");
|
|
18
|
+
};
|
|
19
|
+
const status = async () => {
|
|
20
|
+
const token = await loadToken();
|
|
21
|
+
if (!token) {
|
|
22
|
+
log("Not authenticated. Run `ararahq-mcp login`.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const response = await axios.get(`${getApiBaseUrl()}/auth/me`, {
|
|
27
|
+
headers: { Authorization: `Bearer ${token.accessToken}` },
|
|
28
|
+
timeout: API_TIMEOUT_MS,
|
|
29
|
+
});
|
|
30
|
+
const identity = identitySchema.parse(response.data);
|
|
31
|
+
log(`Authenticated as ${identity.name} (${identity.email}).`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
log("A token exists, but the OAuth session could not be validated. Run `ararahq-mcp login` again.");
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const doctor = async () => {
|
|
39
|
+
log(`AraraHQ MCP ${SERVER_VERSION}`);
|
|
40
|
+
log(`Node ${process.versions.node}: ${Number(process.versions.node.split(".")[0]) >= 20 ? "ok" : "requires >=20"}`);
|
|
41
|
+
log(`API: ${getApiBaseUrl()}`);
|
|
42
|
+
await status();
|
|
43
|
+
};
|
|
44
|
+
export const runCli = async (command) => {
|
|
45
|
+
if (command === "login") {
|
|
46
|
+
await login();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (command === "logout") {
|
|
50
|
+
await clearToken();
|
|
51
|
+
log("OAuth tokens removed from the system keychain.");
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (command === "status") {
|
|
55
|
+
await status();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (command === "doctor") {
|
|
59
|
+
await doctor();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (command === "tools") {
|
|
63
|
+
TOOL_NAMES.forEach(log);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (command === "--version" || command === "-v") {
|
|
67
|
+
log(SERVER_VERSION);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
package/build/config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SERVER_NAME = "ararahq-mcp";
|
|
2
|
+
export const SERVER_VERSION = "5.0.0";
|
|
3
|
+
export const DEFAULT_API_BASE_URL = "https://api.ararahq.com/api";
|
|
4
|
+
export const OAUTH_CLIENT_ID = "ararahq-mcp";
|
|
5
|
+
export const OAUTH_SCOPE = "openid profile organization:read atendimento:read atendimento:write campaigns:write";
|
|
6
|
+
export const API_TIMEOUT_MS = 10_000;
|
|
7
|
+
export const MAX_RETRIES = 2;
|
|
8
|
+
export const MAX_PAGE_SIZE = 100;
|
|
9
|
+
export const getApiBaseUrl = () => (process.env.ARARA_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
10
|
+
export const getHttpPort = () => {
|
|
11
|
+
const raw = process.env.PORT ?? "3333";
|
|
12
|
+
const parsed = Number.parseInt(raw, 10);
|
|
13
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
|
|
14
|
+
throw new Error("PORT must be an integer between 1 and 65535.");
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
};
|
package/build/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { runCli } from "./cli.js";
|
|
4
|
+
import { runHttp } from "./transports/http.js";
|
|
5
|
+
import { runStdio } from "./transports/stdio.js";
|
|
6
|
+
const main = async () => {
|
|
7
|
+
const command = process.argv[2];
|
|
8
|
+
if (command && (await runCli(command)))
|
|
9
|
+
return;
|
|
10
|
+
const useHttp = process.env.MCP_TRANSPORT === "http" ||
|
|
11
|
+
(process.env.PORT !== undefined && !process.argv.includes("--stdio"));
|
|
12
|
+
if (useHttp)
|
|
13
|
+
await runHttp();
|
|
14
|
+
else
|
|
15
|
+
await runStdio();
|
|
16
|
+
};
|
|
17
|
+
if (process.env.NODE_ENV !== "test") {
|
|
18
|
+
main().catch((error) => {
|
|
19
|
+
const message = error instanceof Error ? error.message : "Unknown startup error";
|
|
20
|
+
process.stderr.write(`AraraHQ MCP failed: ${message}\n`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
});
|
|
23
|
+
}
|
package/build/lib/api.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_TIMEOUT_MS, getApiBaseUrl, MAX_RETRIES } from "../config.js";
|
|
3
|
+
import { getAccessToken } from "../auth/tokens.js";
|
|
4
|
+
import { AraraError, toAraraError } from "./errors.js";
|
|
5
|
+
const wait = async (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6
|
+
const canRetry = (method, idempotencyKey) => ["GET", "HEAD", "PUT", "DELETE", "OPTIONS"].includes(method.toUpperCase()) ||
|
|
7
|
+
idempotencyKey !== undefined;
|
|
8
|
+
export const apiRequest = async (path, options) => {
|
|
9
|
+
const token = await getAccessToken();
|
|
10
|
+
const method = options.method ?? "GET";
|
|
11
|
+
const retryAllowed = (options.retry ?? true) && canRetry(method, options.idempotencyKey);
|
|
12
|
+
let attempt = 0;
|
|
13
|
+
while (true) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await axios.request({
|
|
16
|
+
baseURL: getApiBaseUrl(),
|
|
17
|
+
url: path,
|
|
18
|
+
method,
|
|
19
|
+
data: options.body,
|
|
20
|
+
timeout: API_TIMEOUT_MS,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${token}`,
|
|
23
|
+
...(options.idempotencyKey === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { "Idempotency-Key": options.idempotencyKey }),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const parsed = options.schema.safeParse(response.data);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
throw new AraraError("INVALID_API_RESPONSE", "AraraHQ API returned an unexpected response.", 502, false);
|
|
31
|
+
}
|
|
32
|
+
return parsed.data;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const normalized = toAraraError(error);
|
|
36
|
+
if (!retryAllowed || !normalized.retryable || attempt >= MAX_RETRIES)
|
|
37
|
+
throw normalized;
|
|
38
|
+
const delay = normalized.retryAfterSeconds !== undefined
|
|
39
|
+
? normalized.retryAfterSeconds * 1_000
|
|
40
|
+
: 250 * 2 ** attempt + Math.floor(Math.random() * 100);
|
|
41
|
+
attempt += 1;
|
|
42
|
+
await wait(Math.min(delay, 10_000));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export class AraraError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
retryable;
|
|
6
|
+
retryAfterSeconds;
|
|
7
|
+
constructor(code, message, status, retryable, retryAfterSeconds) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.retryable = retryable;
|
|
12
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
13
|
+
this.name = "AraraError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const parseRetryAfter = (value) => {
|
|
17
|
+
if (typeof value !== "string" || value.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const seconds = Number.parseInt(value, 10);
|
|
20
|
+
if (Number.isFinite(seconds))
|
|
21
|
+
return Math.max(0, seconds);
|
|
22
|
+
const date = Date.parse(value);
|
|
23
|
+
return Number.isNaN(date) ? undefined : Math.max(0, Math.ceil((date - Date.now()) / 1_000));
|
|
24
|
+
};
|
|
25
|
+
export const toAraraError = (error) => {
|
|
26
|
+
if (error instanceof AraraError)
|
|
27
|
+
return error;
|
|
28
|
+
if (!axios.isAxiosError(error)) {
|
|
29
|
+
return new AraraError("INTERNAL_ERROR", "The MCP server could not complete the request.", 500, false);
|
|
30
|
+
}
|
|
31
|
+
const status = error.response?.status ?? 503;
|
|
32
|
+
const payload = error.response?.data;
|
|
33
|
+
const candidate = typeof payload === "object" && payload !== null ? payload : {};
|
|
34
|
+
const nested = typeof candidate.error === "object" && candidate.error !== null
|
|
35
|
+
? candidate.error
|
|
36
|
+
: candidate;
|
|
37
|
+
const code = typeof nested.code === "string"
|
|
38
|
+
? nested.code
|
|
39
|
+
: status === 429
|
|
40
|
+
? "RATE_LIMITED"
|
|
41
|
+
: "UPSTREAM_ERROR";
|
|
42
|
+
const message = typeof nested.message === "string"
|
|
43
|
+
? nested.message
|
|
44
|
+
: status === 401
|
|
45
|
+
? "OAuth authentication is required or has expired."
|
|
46
|
+
: "AraraHQ API request failed.";
|
|
47
|
+
const retryAfterSeconds = parseRetryAfter(error.response?.headers["retry-after"]);
|
|
48
|
+
return new AraraError(code, message, status, status === 429 || status >= 500, retryAfterSeconds);
|
|
49
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const jsonValueSchema = z.lazy(() => z.union([
|
|
3
|
+
z.string(),
|
|
4
|
+
z.number(),
|
|
5
|
+
z.boolean(),
|
|
6
|
+
z.null(),
|
|
7
|
+
z.array(jsonValueSchema),
|
|
8
|
+
z.record(jsonValueSchema),
|
|
9
|
+
]));
|
|
10
|
+
export const identitySchema = z
|
|
11
|
+
.object({ name: z.string(), email: z.string().email() })
|
|
12
|
+
.passthrough();
|
|
13
|
+
export const planSchema = z.record(jsonValueSchema);
|
|
14
|
+
export const paginationSchema = z.object({
|
|
15
|
+
page: z.number().int().nonnegative(),
|
|
16
|
+
size: z.number().int().positive(),
|
|
17
|
+
totalElements: z.number().int().nonnegative(),
|
|
18
|
+
totalPages: z.number().int().nonnegative(),
|
|
19
|
+
});
|
|
20
|
+
export const inboxRowSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
id: z.string(),
|
|
23
|
+
customerPhone: z.string(),
|
|
24
|
+
customerName: z.string().nullable().optional(),
|
|
25
|
+
status: z.string(),
|
|
26
|
+
lastInteractionAt: z.string().nullable().optional(),
|
|
27
|
+
windowExpiresAt: z.string().nullable().optional(),
|
|
28
|
+
isWindowOpen: z.boolean(),
|
|
29
|
+
leadSummary: z.string().nullable().optional(),
|
|
30
|
+
originatingCampaignId: z.string().nullable().optional(),
|
|
31
|
+
routineKey: z.string(),
|
|
32
|
+
stage: z.string().nullable().optional(),
|
|
33
|
+
nextStep: z.string().nullable().optional(),
|
|
34
|
+
slaDueAt: z.string().nullable().optional(),
|
|
35
|
+
overdue: z.boolean(),
|
|
36
|
+
ownerId: z.string().nullable().optional(),
|
|
37
|
+
ownerName: z.string().nullable().optional(),
|
|
38
|
+
claimedAt: z.string().nullable().optional(),
|
|
39
|
+
lastMessagePreview: z.string().nullable().optional(),
|
|
40
|
+
lastMessageDirection: z.string().nullable().optional(),
|
|
41
|
+
})
|
|
42
|
+
.passthrough();
|
|
43
|
+
export const pagedInboxSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
content: z.array(inboxRowSchema),
|
|
46
|
+
page: z.number().int().nonnegative(),
|
|
47
|
+
size: z.number().int().positive(),
|
|
48
|
+
totalElements: z.number().int().nonnegative(),
|
|
49
|
+
totalPages: z.number().int().nonnegative(),
|
|
50
|
+
waiting: z.number().int().nonnegative(),
|
|
51
|
+
attention: z.number().int().nonnegative(),
|
|
52
|
+
unassigned: z.number().int().nonnegative(),
|
|
53
|
+
})
|
|
54
|
+
.transform(({ content, page, size, totalElements, totalPages, waiting, attention, unassigned }) => ({
|
|
55
|
+
data: content,
|
|
56
|
+
pagination: { page, size, totalElements, totalPages },
|
|
57
|
+
summary: { waiting, attention, unassigned },
|
|
58
|
+
}));
|
|
59
|
+
export const todaySchema = z.record(jsonValueSchema);
|
|
60
|
+
export const coverageSchema = z.record(jsonValueSchema);
|
|
61
|
+
export const routineSchema = z
|
|
62
|
+
.object({ key: z.string(), name: z.string().optional(), published: z.boolean() })
|
|
63
|
+
.passthrough();
|
|
64
|
+
export const routinesSchema = z.array(routineSchema);
|
|
65
|
+
export const messageSchema = z.object({ id: z.string().nullable() }).passthrough();
|
|
66
|
+
export const pagedMessagesSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
content: z.array(messageSchema),
|
|
69
|
+
page: z.number().int().nonnegative(),
|
|
70
|
+
size: z.number().int().positive(),
|
|
71
|
+
totalElements: z.number().int().nonnegative(),
|
|
72
|
+
totalPages: z.number().int().nonnegative(),
|
|
73
|
+
})
|
|
74
|
+
.transform(({ content, page, size, totalElements, totalPages }) => ({
|
|
75
|
+
data: content,
|
|
76
|
+
pagination: { page, size, totalElements, totalPages },
|
|
77
|
+
}));
|
|
78
|
+
export const mutationSchema = z.record(jsonValueSchema);
|
|
79
|
+
export const automationSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
id: z.string(),
|
|
82
|
+
name: z.string(),
|
|
83
|
+
trigger: z.string(),
|
|
84
|
+
triggerConfig: jsonValueSchema,
|
|
85
|
+
active: z.boolean(),
|
|
86
|
+
steps: z.array(z.object({ type: z.string(), config: jsonValueSchema }).passthrough()),
|
|
87
|
+
costPerRunBrl: z.number(),
|
|
88
|
+
stats: jsonValueSchema.optional(),
|
|
89
|
+
webhookUrl: z.string().nullable().optional(),
|
|
90
|
+
})
|
|
91
|
+
.passthrough();
|
|
92
|
+
export const automationsSchema = z.array(automationSchema);
|
|
93
|
+
export const templateSchema = z
|
|
94
|
+
.object({
|
|
95
|
+
id: z.string(),
|
|
96
|
+
name: z.string(),
|
|
97
|
+
formattedName: z.string().optional(),
|
|
98
|
+
category: z.string(),
|
|
99
|
+
language: z.string(),
|
|
100
|
+
providerStatus: z.string(),
|
|
101
|
+
availableForSending: z.boolean(),
|
|
102
|
+
})
|
|
103
|
+
.passthrough();
|
|
104
|
+
export const templatesSchema = z.array(templateSchema);
|
|
105
|
+
export const numberSchema = z.record(jsonValueSchema);
|
|
106
|
+
export const numbersSchema = z.object({
|
|
107
|
+
numbers: z.array(numberSchema),
|
|
108
|
+
slot: jsonValueSchema.nullable().optional(),
|
|
109
|
+
});
|
|
110
|
+
export const campaignSchema = z
|
|
111
|
+
.object({
|
|
112
|
+
id: z.string(),
|
|
113
|
+
name: z.string(),
|
|
114
|
+
status: z.string(),
|
|
115
|
+
totalMessages: z.number().int().nonnegative(),
|
|
116
|
+
totalCost: z.number(),
|
|
117
|
+
scheduledAt: z.string().nullable().optional(),
|
|
118
|
+
})
|
|
119
|
+
.passthrough();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toAraraError } from "../lib/errors.js";
|
|
3
|
+
export const toolOutputSchema = {
|
|
4
|
+
ok: z.boolean(),
|
|
5
|
+
data: z.unknown().optional(),
|
|
6
|
+
error: z
|
|
7
|
+
.object({
|
|
8
|
+
code: z.string(),
|
|
9
|
+
message: z.string(),
|
|
10
|
+
retryable: z.boolean(),
|
|
11
|
+
retryAfterSeconds: z.number().optional(),
|
|
12
|
+
})
|
|
13
|
+
.optional(),
|
|
14
|
+
};
|
|
15
|
+
export const success = (data, message) => ({
|
|
16
|
+
content: [{ type: "text", text: message }],
|
|
17
|
+
structuredContent: { ok: true, data },
|
|
18
|
+
});
|
|
19
|
+
export const failure = (error) => {
|
|
20
|
+
const normalized = toAraraError(error);
|
|
21
|
+
const details = {
|
|
22
|
+
code: normalized.code,
|
|
23
|
+
message: normalized.message,
|
|
24
|
+
retryable: normalized.retryable,
|
|
25
|
+
...(normalized.retryAfterSeconds === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { retryAfterSeconds: normalized.retryAfterSeconds }),
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
isError: true,
|
|
31
|
+
content: [{ type: "text", text: `${details.code}: ${details.message}` }],
|
|
32
|
+
structuredContent: { ok: false, error: details },
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export const execute = async (task) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await task();
|
|
38
|
+
return success(result.data, result.message);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return failure(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const prompt = (text) => ({
|
|
2
|
+
messages: [{ role: "user", content: { type: "text", text } }],
|
|
3
|
+
});
|
|
4
|
+
export const registerAllPrompts = (server) => {
|
|
5
|
+
server.registerPrompt("triage_today", { description: "Prioritize today's Atendimento queue without taking write actions." }, () => prompt("Read arara://operation/health, call get_today, and propose a prioritized queue. Show overdue items, unowned conversations, stage, owner, next step and SLA. Do not claim, reply or close anything without explicit confirmation."));
|
|
6
|
+
server.registerPrompt("draft_reply", { description: "Draft a grounded reply for one conversation." }, () => prompt("Use find_conversations and get_conversation to understand the selected case. Draft a concise reply consistent with the current stage and next step. Do not call reply_to_conversation until the user explicitly approves the exact text."));
|
|
7
|
+
server.registerPrompt("campaign_preflight", { description: "Review a campaign before publication." }, () => prompt("Call prepare_campaign. Verify the approved template, published Atendimento routine, coverage, audience, variables and schedule. Present the final payload and risks. Do not call publish_campaign without explicit confirmation."));
|
|
8
|
+
server.registerPrompt("close_case_review", { description: "Check whether a conversation is ready to close." }, () => prompt("Load the conversation timeline and its metadata. Confirm owner, outcome, next step and any promised follow-up. Recommend closure only when no unresolved obligation remains. Do not call close_conversation without explicit confirmation."));
|
|
9
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { apiRequest } from "../lib/api.js";
|
|
2
|
+
import { coverageSchema, identitySchema, numbersSchema, planSchema, routineSchema, templatesSchema, todaySchema, } from "../lib/schemas.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const json = (value) => JSON.stringify(value, null, 2);
|
|
5
|
+
const routinesResponseSchema = z.object({ data: z.array(routineSchema) });
|
|
6
|
+
const registerJsonResource = (server, name, uri, title, description, loader) => {
|
|
7
|
+
server.registerResource(name, uri, { title, description, mimeType: "application/json" }, async (resourceUri) => {
|
|
8
|
+
try {
|
|
9
|
+
return {
|
|
10
|
+
contents: [
|
|
11
|
+
{
|
|
12
|
+
uri: resourceUri.toString(),
|
|
13
|
+
mimeType: "application/json",
|
|
14
|
+
text: json(await loader()),
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {
|
|
21
|
+
contents: [
|
|
22
|
+
{
|
|
23
|
+
uri: resourceUri.toString(),
|
|
24
|
+
mimeType: "application/json",
|
|
25
|
+
text: json({
|
|
26
|
+
error: { code: "RESOURCE_UNAVAILABLE", message: "Resource could not be loaded." },
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
export const registerAllResources = (server) => {
|
|
35
|
+
registerJsonResource(server, "organization", "arara://organization", "Organization", "Authenticated identity and current AraraHQ plan.", async () => {
|
|
36
|
+
const [identity, plan] = await Promise.all([
|
|
37
|
+
apiRequest("/auth/me", { schema: identitySchema }),
|
|
38
|
+
apiRequest("/v1/organizations/me/plan", { schema: planSchema }),
|
|
39
|
+
]);
|
|
40
|
+
return { identity, plan };
|
|
41
|
+
});
|
|
42
|
+
registerJsonResource(server, "operation_health", "arara://operation/health", "Operation health", "Today, channel health and published Atendimento routines.", async () => {
|
|
43
|
+
const [today, numbers, routines] = await Promise.all([
|
|
44
|
+
apiRequest("/v1/operation/today", { schema: todaySchema }),
|
|
45
|
+
apiRequest("/v1/organizations/me/numbers", { schema: numbersSchema }),
|
|
46
|
+
apiRequest("/v1/operation/routines", { schema: routinesResponseSchema }),
|
|
47
|
+
]);
|
|
48
|
+
return {
|
|
49
|
+
today,
|
|
50
|
+
channels: numbers,
|
|
51
|
+
publishedRoutines: routines.data.filter((routine) => routine.published),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
registerJsonResource(server, "approved_templates", "arara://templates/approved", "Approved templates", "Templates that Meta currently allows for sending.", async () => {
|
|
55
|
+
const templates = await apiRequest("/v1/templates", { schema: templatesSchema });
|
|
56
|
+
return templates.filter((template) => template.providerStatus === "APPROVED" && template.availableForSending);
|
|
57
|
+
});
|
|
58
|
+
registerJsonResource(server, "channels", "arara://channels", "WhatsApp channels", "Configured numbers and slot state for this organization.", () => apiRequest("/v1/organizations/me/numbers", { schema: numbersSchema }));
|
|
59
|
+
registerJsonResource(server, "coverage", "arara://coverage", "Service coverage", "Working hours and after-hours policy for Atendimento.", () => apiRequest("/v1/operation/coverage", { schema: coverageSchema }));
|
|
60
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { SERVER_NAME, SERVER_VERSION } from "../config.js";
|
|
3
|
+
import { registerAllPrompts } from "../prompts/index.js";
|
|
4
|
+
import { registerAllResources } from "../resources/index.js";
|
|
5
|
+
import { registerAllTools } from "../tools/index.js";
|
|
6
|
+
export const createServer = () => {
|
|
7
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
8
|
+
registerAllTools(server);
|
|
9
|
+
registerAllResources(server);
|
|
10
|
+
registerAllPrompts(server);
|
|
11
|
+
return server;
|
|
12
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { MAX_PAGE_SIZE } from "../config.js";
|
|
4
|
+
import { apiRequest } from "../lib/api.js";
|
|
5
|
+
import { AraraError } from "../lib/errors.js";
|
|
6
|
+
import { automationSchema, automationsSchema, campaignSchema, coverageSchema, identitySchema, jsonValueSchema, messageSchema, mutationSchema, pagedInboxSchema, pagedMessagesSchema, planSchema, routineSchema, templateSchema, templatesSchema, todaySchema, } from "../lib/schemas.js";
|
|
7
|
+
import { execute, toolOutputSchema } from "../mcp/result.js";
|
|
8
|
+
const readOnly = {
|
|
9
|
+
readOnlyHint: true,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: true,
|
|
12
|
+
openWorldHint: true,
|
|
13
|
+
};
|
|
14
|
+
const write = {
|
|
15
|
+
readOnlyHint: false,
|
|
16
|
+
destructiveHint: false,
|
|
17
|
+
idempotentHint: false,
|
|
18
|
+
openWorldHint: true,
|
|
19
|
+
};
|
|
20
|
+
const idempotentWrite = { ...write, idempotentHint: true };
|
|
21
|
+
const destructive = {
|
|
22
|
+
readOnlyHint: false,
|
|
23
|
+
destructiveHint: true,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
};
|
|
27
|
+
const idSchema = z.string().uuid();
|
|
28
|
+
const e164Schema = z.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164, for example +5511999999999.");
|
|
29
|
+
const routineKeySchema = z.enum(["support", "billing", "scheduling"]);
|
|
30
|
+
const routinesResponseSchema = z.object({ data: z.array(routineSchema) });
|
|
31
|
+
const register = (server, name, description, inputSchema, annotations, handler) => {
|
|
32
|
+
server.registerTool(name, { description, inputSchema, outputSchema: toolOutputSchema, annotations }, handler);
|
|
33
|
+
};
|
|
34
|
+
export const TOOL_NAMES = [
|
|
35
|
+
"whoami",
|
|
36
|
+
"get_today",
|
|
37
|
+
"find_conversations",
|
|
38
|
+
"get_conversation",
|
|
39
|
+
"reply_to_conversation",
|
|
40
|
+
"claim_conversation",
|
|
41
|
+
"close_conversation",
|
|
42
|
+
"list_automations",
|
|
43
|
+
"get_automation",
|
|
44
|
+
"prepare_campaign",
|
|
45
|
+
"publish_campaign",
|
|
46
|
+
"send_whatsapp",
|
|
47
|
+
"check_message",
|
|
48
|
+
"save_contacts",
|
|
49
|
+
"create_template",
|
|
50
|
+
"get_template_status",
|
|
51
|
+
"opt_out",
|
|
52
|
+
];
|
|
53
|
+
export const registerAllTools = (server) => {
|
|
54
|
+
register(server, "whoami", "Return the authenticated AraraHQ identity and organization plan.", {}, readOnly, async () => execute(async () => {
|
|
55
|
+
const [identity, plan] = await Promise.all([
|
|
56
|
+
apiRequest("/auth/me", { schema: identitySchema }),
|
|
57
|
+
apiRequest("/v1/organizations/me/plan", { schema: planSchema }),
|
|
58
|
+
]);
|
|
59
|
+
return {
|
|
60
|
+
data: { identity, plan },
|
|
61
|
+
message: `Authenticated as ${identity.name} (${identity.email}).`,
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
register(server, "get_today", "Return today's Atendimento queue across support, billing and scheduling.", {}, readOnly, async () => execute(async () => ({
|
|
65
|
+
data: await apiRequest("/v1/operation/today", { schema: todaySchema }),
|
|
66
|
+
message: "Today's operation loaded.",
|
|
67
|
+
})));
|
|
68
|
+
register(server, "find_conversations", "Find Atendimento conversations with stable pagination. Use this before selecting a conversation.", {
|
|
69
|
+
filter: z.string().max(100).optional(),
|
|
70
|
+
page: z.number().int().nonnegative().default(0),
|
|
71
|
+
size: z.number().int().min(1).max(MAX_PAGE_SIZE).default(50),
|
|
72
|
+
}, readOnly, async (input) => execute(async () => {
|
|
73
|
+
const params = new URLSearchParams({ page: String(input.page), size: String(input.size) });
|
|
74
|
+
if (typeof input.filter === "string" && input.filter.length > 0)
|
|
75
|
+
params.set("filter", input.filter);
|
|
76
|
+
const data = await apiRequest(`/v1/operation/inbox?${params.toString()}`, {
|
|
77
|
+
schema: pagedInboxSchema,
|
|
78
|
+
});
|
|
79
|
+
return { data, message: `${data.data.length} conversation(s) loaded.` };
|
|
80
|
+
}));
|
|
81
|
+
register(server, "get_conversation", "Load a paginated message timeline. Conversation metadata comes from find_conversations.", {
|
|
82
|
+
conversationId: idSchema,
|
|
83
|
+
page: z.number().int().nonnegative().default(0),
|
|
84
|
+
size: z.number().int().min(1).max(MAX_PAGE_SIZE).default(50),
|
|
85
|
+
}, readOnly, async (input) => execute(async () => {
|
|
86
|
+
const conversationId = idSchema.parse(input.conversationId);
|
|
87
|
+
const page = z.number().int().nonnegative().parse(input.page);
|
|
88
|
+
const size = z.number().int().min(1).max(MAX_PAGE_SIZE).parse(input.size);
|
|
89
|
+
const data = await apiRequest(`/v1/conversations/${conversationId}/messages?page=${page}&size=${size}`, { schema: pagedMessagesSchema });
|
|
90
|
+
return {
|
|
91
|
+
data: { conversationId, ...data },
|
|
92
|
+
message: `${data.data.length} message(s) loaded.`,
|
|
93
|
+
};
|
|
94
|
+
}));
|
|
95
|
+
register(server, "reply_to_conversation", "Reply inside an Atendimento conversation. Accepted means queued, not delivered.", {
|
|
96
|
+
conversationId: idSchema,
|
|
97
|
+
body: z.string().trim().min(1).max(4096),
|
|
98
|
+
}, write, async (input) => execute(async () => ({
|
|
99
|
+
data: await apiRequest("/v1/conversations/reply", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: input,
|
|
102
|
+
schema: mutationSchema,
|
|
103
|
+
retry: false,
|
|
104
|
+
}),
|
|
105
|
+
message: "Reply accepted and queued for delivery.",
|
|
106
|
+
})));
|
|
107
|
+
register(server, "claim_conversation", "Assign an unowned conversation to the authenticated operator.", { conversationId: idSchema }, idempotentWrite, async (input) => execute(async () => {
|
|
108
|
+
const conversationId = idSchema.parse(input.conversationId);
|
|
109
|
+
return {
|
|
110
|
+
data: await apiRequest(`/v1/operation/inbox/${conversationId}/claim`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: {},
|
|
113
|
+
schema: mutationSchema,
|
|
114
|
+
retry: false,
|
|
115
|
+
}),
|
|
116
|
+
message: "Conversation claimed.",
|
|
117
|
+
};
|
|
118
|
+
}));
|
|
119
|
+
register(server, "close_conversation", "Close a conversation after its outcome and next step are resolved.", { conversationId: idSchema }, destructive, async (input) => execute(async () => {
|
|
120
|
+
const conversationId = idSchema.parse(input.conversationId);
|
|
121
|
+
return {
|
|
122
|
+
data: await apiRequest(`/v1/conversations/${conversationId}/status`, {
|
|
123
|
+
method: "PATCH",
|
|
124
|
+
body: { status: "CLOSED" },
|
|
125
|
+
schema: mutationSchema,
|
|
126
|
+
}),
|
|
127
|
+
message: "Conversation closed.",
|
|
128
|
+
};
|
|
129
|
+
}));
|
|
130
|
+
register(server, "list_automations", "List configured automations.", {}, readOnly, async () => execute(async () => {
|
|
131
|
+
const data = await apiRequest("/v1/automations", { schema: automationsSchema });
|
|
132
|
+
return { data, message: `${data.length} automation(s) loaded.` };
|
|
133
|
+
}));
|
|
134
|
+
register(server, "get_automation", "Inspect an automation's trigger, steps, cost and status.", { automationId: idSchema }, readOnly, async (input) => execute(async () => {
|
|
135
|
+
const automationId = idSchema.parse(input.automationId);
|
|
136
|
+
const data = await apiRequest(`/v1/automations/${automationId}`, {
|
|
137
|
+
schema: automationSchema,
|
|
138
|
+
});
|
|
139
|
+
return { data, message: `Automation ${data.name} loaded.` };
|
|
140
|
+
}));
|
|
141
|
+
register(server, "prepare_campaign", "Load campaign preflight: guidance, published Atendimento routines, approved templates and coverage.", {}, readOnly, async () => execute(async () => {
|
|
142
|
+
const [copilot, routines, templates, coverage] = await Promise.all([
|
|
143
|
+
apiRequest("/v1/operation/copilot/campaign", { schema: z.record(jsonValueSchema) }),
|
|
144
|
+
apiRequest("/v1/operation/routines", { schema: routinesResponseSchema }),
|
|
145
|
+
apiRequest("/v1/templates", { schema: templatesSchema }),
|
|
146
|
+
apiRequest("/v1/operation/coverage", { schema: coverageSchema }),
|
|
147
|
+
]);
|
|
148
|
+
return {
|
|
149
|
+
data: {
|
|
150
|
+
copilot,
|
|
151
|
+
coverage,
|
|
152
|
+
publishedRoutines: routines.data.filter((routine) => routine.published),
|
|
153
|
+
approvedTemplates: templates.filter((template) => template.providerStatus === "APPROVED" && template.availableForSending),
|
|
154
|
+
},
|
|
155
|
+
message: "Campaign preflight loaded. Review it before publishing.",
|
|
156
|
+
};
|
|
157
|
+
}));
|
|
158
|
+
const campaignContactSchema = z.object({
|
|
159
|
+
to: e164Schema,
|
|
160
|
+
variables: z.array(z.string().max(1024)).max(20).default([]),
|
|
161
|
+
});
|
|
162
|
+
register(server, "publish_campaign", "Publish a template campaign after validating its template and Atendimento destination routine.", {
|
|
163
|
+
name: z.string().trim().min(1).max(255),
|
|
164
|
+
templateName: z.string().trim().min(1),
|
|
165
|
+
routineKey: routineKeySchema,
|
|
166
|
+
contacts: z.array(campaignContactSchema).min(1).max(1000),
|
|
167
|
+
sender: e164Schema.optional(),
|
|
168
|
+
scheduledAt: z.string().datetime().optional(),
|
|
169
|
+
idempotencyKey: z.string().uuid().optional(),
|
|
170
|
+
}, write, async (input) => execute(async () => {
|
|
171
|
+
const [routines, templates] = await Promise.all([
|
|
172
|
+
apiRequest("/v1/operation/routines", { schema: routinesResponseSchema }),
|
|
173
|
+
apiRequest("/v1/templates", { schema: templatesSchema }),
|
|
174
|
+
]);
|
|
175
|
+
const routineKey = routineKeySchema.parse(input.routineKey);
|
|
176
|
+
if (!routines.data.some((routine) => routine.key === routineKey && routine.published)) {
|
|
177
|
+
throw new AraraError("ROUTINE_NOT_PUBLISHED", `Atendimento routine '${routineKey}' is not published.`, 409, false);
|
|
178
|
+
}
|
|
179
|
+
if (!templates.some((template) => template.name === input.templateName &&
|
|
180
|
+
template.providerStatus === "APPROVED" &&
|
|
181
|
+
template.availableForSending)) {
|
|
182
|
+
throw new AraraError("TEMPLATE_NOT_AVAILABLE", `Template '${String(input.templateName)}' is not approved and available.`, 409, false);
|
|
183
|
+
}
|
|
184
|
+
const idempotencyKey = typeof input.idempotencyKey === "string" ? input.idempotencyKey : randomUUID();
|
|
185
|
+
const data = await apiRequest("/v1/campaigns", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: {
|
|
188
|
+
name: input.name,
|
|
189
|
+
templateName: input.templateName,
|
|
190
|
+
routineKey,
|
|
191
|
+
contacts: input.contacts,
|
|
192
|
+
...(input.sender === undefined ? {} : { sender: input.sender }),
|
|
193
|
+
...(input.scheduledAt === undefined ? {} : { scheduledAt: input.scheduledAt }),
|
|
194
|
+
},
|
|
195
|
+
schema: campaignSchema,
|
|
196
|
+
idempotencyKey,
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
data: { ...data, idempotencyKey },
|
|
200
|
+
message: `Campaign '${data.name}' accepted with ${data.totalMessages} message(s).`,
|
|
201
|
+
};
|
|
202
|
+
}));
|
|
203
|
+
register(server, "send_whatsapp", "Send one WhatsApp text message to an E.164 number. Accepted means queued, not delivered.", {
|
|
204
|
+
to: e164Schema,
|
|
205
|
+
message: z.string().trim().min(1).max(4096),
|
|
206
|
+
from: e164Schema.optional(),
|
|
207
|
+
idempotencyKey: z.string().uuid().optional(),
|
|
208
|
+
}, write, async (input) => execute(async () => {
|
|
209
|
+
const idempotencyKey = typeof input.idempotencyKey === "string" ? input.idempotencyKey : randomUUID();
|
|
210
|
+
const data = await apiRequest("/v1/messages", {
|
|
211
|
+
method: "POST",
|
|
212
|
+
body: {
|
|
213
|
+
receiver: input.to,
|
|
214
|
+
body: input.message,
|
|
215
|
+
type: "text",
|
|
216
|
+
...(input.from === undefined ? {} : { sender: input.from }),
|
|
217
|
+
},
|
|
218
|
+
schema: messageSchema,
|
|
219
|
+
idempotencyKey,
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
data: { ...data, idempotencyKey },
|
|
223
|
+
message: `Message ${data.id ?? "accepted"} queued. Use check_message to verify delivery.`,
|
|
224
|
+
};
|
|
225
|
+
}));
|
|
226
|
+
register(server, "check_message", "Check the provider delivery state for a message.", { messageId: idSchema }, readOnly, async (input) => execute(async () => {
|
|
227
|
+
const messageId = idSchema.parse(input.messageId);
|
|
228
|
+
return {
|
|
229
|
+
data: await apiRequest(`/v1/messages/${messageId}`, { schema: mutationSchema }),
|
|
230
|
+
message: "Message status loaded.",
|
|
231
|
+
};
|
|
232
|
+
}));
|
|
233
|
+
const contactSchema = z.object({
|
|
234
|
+
name: z.string().trim().min(1).max(255),
|
|
235
|
+
phone: e164Schema,
|
|
236
|
+
email: z.string().email().max(255).optional(),
|
|
237
|
+
attributes: z.record(jsonValueSchema).optional(),
|
|
238
|
+
});
|
|
239
|
+
register(server, "save_contacts", "Create or update contacts in one batch.", { contacts: z.array(contactSchema).min(1).max(1000) }, idempotentWrite, async (input) => execute(async () => ({
|
|
240
|
+
data: await apiRequest("/v1/contacts/batch", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
body: input.contacts,
|
|
243
|
+
schema: mutationSchema,
|
|
244
|
+
retry: false,
|
|
245
|
+
}),
|
|
246
|
+
message: "Contact batch processed.",
|
|
247
|
+
})));
|
|
248
|
+
register(server, "create_template", "Submit a WhatsApp template for Meta approval.", {
|
|
249
|
+
name: z
|
|
250
|
+
.string()
|
|
251
|
+
.regex(/^[a-z0-9_]+$/)
|
|
252
|
+
.max(512),
|
|
253
|
+
category: z.enum(["MARKETING", "UTILITY", "AUTHENTICATION"]),
|
|
254
|
+
body: z.string().trim().min(1).max(4096),
|
|
255
|
+
language: z.string().default("pt_BR"),
|
|
256
|
+
footer: z.string().max(60).optional(),
|
|
257
|
+
}, write, async (input) => execute(async () => ({
|
|
258
|
+
data: await apiRequest("/v1/templates", {
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: input,
|
|
261
|
+
schema: templateSchema,
|
|
262
|
+
retry: false,
|
|
263
|
+
}),
|
|
264
|
+
message: "Template submitted for approval.",
|
|
265
|
+
})));
|
|
266
|
+
register(server, "get_template_status", "Check the Meta approval status for a template.", { templateId: idSchema }, readOnly, async (input) => execute(async () => {
|
|
267
|
+
const templateId = idSchema.parse(input.templateId);
|
|
268
|
+
return {
|
|
269
|
+
data: await apiRequest(`/v1/templates/${templateId}/status`, {
|
|
270
|
+
schema: mutationSchema,
|
|
271
|
+
}),
|
|
272
|
+
message: "Template status loaded.",
|
|
273
|
+
};
|
|
274
|
+
}));
|
|
275
|
+
register(server, "opt_out", "Record a WhatsApp opt-out and suppress future sends.", { phone: e164Schema, reason: z.string().trim().min(1).max(80).optional() }, destructive, async (input) => execute(async () => ({
|
|
276
|
+
data: await apiRequest("/v1/opt-outs", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: input,
|
|
279
|
+
schema: mutationSchema,
|
|
280
|
+
retry: false,
|
|
281
|
+
}),
|
|
282
|
+
message: "Opt-out recorded.",
|
|
283
|
+
})));
|
|
284
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import rateLimit from "express-rate-limit";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { runWithAccessToken } from "../auth/context.js";
|
|
6
|
+
import { API_TIMEOUT_MS, getApiBaseUrl, getHttpPort, SERVER_NAME, SERVER_VERSION, } from "../config.js";
|
|
7
|
+
import { identitySchema } from "../lib/schemas.js";
|
|
8
|
+
import { createServer } from "../server/create.js";
|
|
9
|
+
const splitEnv = (name, fallback) => (process.env[name]?.split(",") ?? fallback)
|
|
10
|
+
.map((item) => item.trim().toLowerCase())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
const extractBearer = (request) => {
|
|
13
|
+
const header = request.header("authorization");
|
|
14
|
+
if (typeof header !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const match = /^Bearer ([^\s]+)$/i.exec(header.trim());
|
|
17
|
+
return match?.[1] ?? null;
|
|
18
|
+
};
|
|
19
|
+
const validateRequestOrigin = (request, response) => {
|
|
20
|
+
const allowedHosts = splitEnv("MCP_ALLOWED_HOSTS", ["localhost", "127.0.0.1", "mcp.ararahq.com"]);
|
|
21
|
+
const hostname = request.hostname.toLowerCase();
|
|
22
|
+
if (!allowedHosts.includes(hostname)) {
|
|
23
|
+
response
|
|
24
|
+
.status(403)
|
|
25
|
+
.json({ error: { code: "HOST_NOT_ALLOWED", message: "Host is not allowed." } });
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const origin = request.header("origin");
|
|
29
|
+
if (!origin)
|
|
30
|
+
return true;
|
|
31
|
+
const allowedOrigins = splitEnv("MCP_ALLOWED_ORIGINS", [
|
|
32
|
+
"https://chatgpt.com",
|
|
33
|
+
"https://claude.ai",
|
|
34
|
+
]);
|
|
35
|
+
let normalized;
|
|
36
|
+
try {
|
|
37
|
+
normalized = new URL(origin).origin.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
response
|
|
41
|
+
.status(403)
|
|
42
|
+
.json({ error: { code: "ORIGIN_NOT_ALLOWED", message: "Origin is not allowed." } });
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!allowedOrigins.includes(normalized)) {
|
|
46
|
+
response
|
|
47
|
+
.status(403)
|
|
48
|
+
.json({ error: { code: "ORIGIN_NOT_ALLOWED", message: "Origin is not allowed." } });
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const validateToken = async (token) => {
|
|
54
|
+
try {
|
|
55
|
+
const response = await axios.get(`${getApiBaseUrl()}/auth/me`, {
|
|
56
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
57
|
+
timeout: API_TIMEOUT_MS,
|
|
58
|
+
});
|
|
59
|
+
return identitySchema.safeParse(response.data).success;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export const runHttp = async () => {
|
|
66
|
+
const app = express();
|
|
67
|
+
app.disable("x-powered-by");
|
|
68
|
+
app.set("trust proxy", 1);
|
|
69
|
+
app.use(express.json({ limit: "64kb", strict: true }));
|
|
70
|
+
app.use(rateLimit({ windowMs: 60_000, limit: 120, standardHeaders: "draft-8", legacyHeaders: false }));
|
|
71
|
+
app.use((request, response, next) => {
|
|
72
|
+
if (!validateRequestOrigin(request, response))
|
|
73
|
+
return;
|
|
74
|
+
const origin = request.header("origin");
|
|
75
|
+
if (origin) {
|
|
76
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
77
|
+
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
78
|
+
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, MCP-Protocol-Version");
|
|
79
|
+
response.setHeader("Vary", "Origin");
|
|
80
|
+
}
|
|
81
|
+
if (request.method === "OPTIONS") {
|
|
82
|
+
response.status(204).end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
app.get("/health", (_request, response) => response.json({ status: "ok", name: SERVER_NAME, version: SERVER_VERSION }));
|
|
88
|
+
const protectedResource = (request, response) => {
|
|
89
|
+
const protocol = request.header("x-forwarded-proto") ?? request.protocol;
|
|
90
|
+
const resource = process.env.MCP_PUBLIC_URL ?? `${protocol}://${request.get("host")}/mcp`;
|
|
91
|
+
response.json({
|
|
92
|
+
resource,
|
|
93
|
+
authorization_servers: [process.env.ARARA_OAUTH_ISSUER ?? getApiBaseUrl()],
|
|
94
|
+
bearer_methods_supported: ["header"],
|
|
95
|
+
scopes_supported: process.env.ARARA_OAUTH_SCOPES?.split(" ") ?? [],
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
app.get("/.well-known/oauth-protected-resource", protectedResource);
|
|
99
|
+
app.get("/.well-known/oauth-protected-resource/mcp", protectedResource);
|
|
100
|
+
const handleMcp = async (request, response) => {
|
|
101
|
+
const token = extractBearer(request);
|
|
102
|
+
if (!token || !(await validateToken(token))) {
|
|
103
|
+
const metadataUrl = process.env.MCP_RESOURCE_METADATA_URL ??
|
|
104
|
+
"https://mcp.ararahq.com/.well-known/oauth-protected-resource/mcp";
|
|
105
|
+
response.setHeader("WWW-Authenticate", `Bearer resource_metadata="${metadataUrl}"`);
|
|
106
|
+
response.status(401).json({
|
|
107
|
+
error: { code: "UNAUTHENTICATED", message: "A valid OAuth bearer token is required." },
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const server = createServer();
|
|
112
|
+
const transport = new StreamableHTTPServerTransport();
|
|
113
|
+
response.on("close", () => {
|
|
114
|
+
void transport.close();
|
|
115
|
+
void server.close();
|
|
116
|
+
});
|
|
117
|
+
await server.connect(transport);
|
|
118
|
+
await runWithAccessToken(token, () => transport.handleRequest(request, response, request.body));
|
|
119
|
+
};
|
|
120
|
+
app.post("/mcp", (request, response) => {
|
|
121
|
+
void handleMcp(request, response).catch(() => {
|
|
122
|
+
if (!response.headersSent) {
|
|
123
|
+
response
|
|
124
|
+
.status(500)
|
|
125
|
+
.json({ error: { code: "INTERNAL_ERROR", message: "Request failed." } });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
app.get("/mcp", (_request, response) => response.status(405).set("Allow", "POST").end());
|
|
130
|
+
app.delete("/mcp", (_request, response) => response.status(405).set("Allow", "POST").end());
|
|
131
|
+
app.use((_request, response) => response.status(404).json({ error: { code: "NOT_FOUND", message: "Not found." } }));
|
|
132
|
+
const port = getHttpPort();
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
const listener = app.listen(port, () => {
|
|
135
|
+
process.stderr.write(`AraraHQ MCP ${SERVER_VERSION} listening on ${port}.\n`);
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
listener.on("error", reject);
|
|
139
|
+
});
|
|
140
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ararahq/mcp",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "The official AraraHQ MCP server for operating WhatsApp support, billing and scheduling with OAuth.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ararahq-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "node --eval \"require('node:fs').rmSync('build',{recursive:true,force:true})\"",
|
|
17
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"format": "prettier --write .",
|
|
23
|
+
"format:check": "prettier --check .",
|
|
24
|
+
"check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build",
|
|
25
|
+
"start": "node build/index.js",
|
|
26
|
+
"dev": "tsx watch src/index.ts --stdio",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"whatsapp",
|
|
36
|
+
"claude",
|
|
37
|
+
"claude-code",
|
|
38
|
+
"cursor",
|
|
39
|
+
"windsurf",
|
|
40
|
+
"chatgpt",
|
|
41
|
+
"ararahq",
|
|
42
|
+
"customer-support",
|
|
43
|
+
"customer-service",
|
|
44
|
+
"atendimento",
|
|
45
|
+
"ai-agent",
|
|
46
|
+
"brazil"
|
|
47
|
+
],
|
|
48
|
+
"homepage": "https://docs.ararahq.com/mcp-server",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/ararahq/mcp.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/ararahq/mcp/issues"
|
|
55
|
+
},
|
|
56
|
+
"author": "AraraHQ <devs@ararahq.com>",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
63
|
+
"axios": "^1.18.0",
|
|
64
|
+
"dotenv": "^17.3.1",
|
|
65
|
+
"express": "^4.22.1",
|
|
66
|
+
"express-rate-limit": "^8.2.1",
|
|
67
|
+
"keytar": "^7.9.0",
|
|
68
|
+
"zod": "^3.25.76"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@eslint/js": "^9.39.2",
|
|
72
|
+
"@types/express": "^4.17.21",
|
|
73
|
+
"@types/node": "^22.19.11",
|
|
74
|
+
"eslint": "^9.39.2",
|
|
75
|
+
"prettier": "^3.8.1",
|
|
76
|
+
"tsx": "^4.21.0",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
78
|
+
"typescript-eslint": "^8.56.1",
|
|
79
|
+
"vitest": "^3.2.4"
|
|
80
|
+
}
|
|
81
|
+
}
|