@trillet-ai/mcp 0.7.3
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 +108 -0
- package/dist/client.js +116 -0
- package/dist/helpers.js +48 -0
- package/dist/index.js +64 -0
- package/dist/registry.js +106 -0
- package/dist/textcall/deps.js +80 -0
- package/dist/textcall/session.js +209 -0
- package/dist/textcall/worker.js +101 -0
- package/dist/tools/account.js +86 -0
- package/dist/tools/agents.js +178 -0
- package/dist/tools/callFlows.js +337 -0
- package/dist/tools/calls.js +391 -0
- package/dist/tools/flowSettings.js +406 -0
- package/dist/tools/knowledgeBases.js +143 -0
- package/dist/tools/nodeFlows.js +317 -0
- package/dist/tools/phoneNumbers.js +221 -0
- package/dist/tools/secrets.js +45 -0
- package/dist/tools/textCalls.js +223 -0
- package/dist/tools/workflows.js +415 -0
- package/dist/version.js +7 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trillet AI
|
|
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,108 @@
|
|
|
1
|
+
# Trillet MCP Server
|
|
2
|
+
|
|
3
|
+
The official [Model Context Protocol](https://modelcontextprotocol.io) server for [Trillet AI](https://trillet.ai). Build and operate production voice agents from Claude: create agents, write call flows, wire in-call API tools, attach knowledge bases, configure post-call analysis, automate follow-ups with workflows, and debug real calls from their transcripts.
|
|
4
|
+
|
|
5
|
+
Built with the official TypeScript MCP SDK, stdio transport. 71 tools, all annotated with read-only/destructive hints.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd Trillet-MCP
|
|
11
|
+
npm install
|
|
12
|
+
npm run build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You need two values from the Trillet dashboard:
|
|
16
|
+
|
|
17
|
+
| Variable | Where to find it |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `TRILLET_API_KEY` | Settings > API Keys |
|
|
20
|
+
| `TRILLET_WORKSPACE_ID` | Workspace settings (24-char hex id) |
|
|
21
|
+
|
|
22
|
+
Optional: `TRILLET_API_URL` (defaults to `https://api.trillet.ai`).
|
|
23
|
+
|
|
24
|
+
### npx (after npm publish)
|
|
25
|
+
|
|
26
|
+
The package is not on npm until someone with the `trillet-ai` org runs the steps in [PUBLISH.md](./PUBLISH.md). After that:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
TRILLET_API_KEY=<your key> TRILLET_WORKSPACE_ID=<your workspace id> npx -y @trillet-ai/mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Cursor and Grok Bot install it from the public plugin repo [TrilletAI/Trillet-grok](https://github.com/TrilletAI/Trillet-grok) (`npx -y @trillet-ai/mcp@latest`, credentials from Plugins → Configure, not from the repo). The same tree is in `cursor-plugin/` here.
|
|
33
|
+
|
|
34
|
+
### Claude Code
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add trillet \
|
|
38
|
+
-e TRILLET_API_KEY=<your key> \
|
|
39
|
+
-e TRILLET_WORKSPACE_ID=<your workspace id> \
|
|
40
|
+
-- node /absolute/path/to/Trillet-MCP/dist/index.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Claude Desktop
|
|
44
|
+
|
|
45
|
+
Add to `claude_desktop_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"trillet": {
|
|
51
|
+
"command": "node",
|
|
52
|
+
"args": ["/absolute/path/to/Trillet-MCP/dist/index.js"],
|
|
53
|
+
"env": {
|
|
54
|
+
"TRILLET_API_KEY": "<your key>",
|
|
55
|
+
"TRILLET_WORKSPACE_ID": "<your workspace id>"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Agency accounts: every tool accepts an optional `workspace_id` argument that overrides the default workspace for that call, so one server instance can manage client workspaces (the key's user must be a member of each).
|
|
63
|
+
|
|
64
|
+
## Tools
|
|
65
|
+
|
|
66
|
+
**Agents & voices** — `trillet_list_agents`, `trillet_get_agent`, `trillet_create_agent`, `trillet_update_agent`, `trillet_delete_agent` (requires `confirm: true`), `trillet_search_voices` (full multi-provider catalog)
|
|
67
|
+
|
|
68
|
+
**Call flows** — `trillet_list_call_flows`, `trillet_get_call_flow`, `trillet_create_call_flow`, `trillet_update_call_flow`, `trillet_update_prompt`, `trillet_set_incall_api_tools`, `trillet_set_post_call_analysis`, `trillet_list_drafts`, `trillet_deactivate_drafts`, `trillet_publish_draft`, `trillet_duplicate_call_flow`, `trillet_archive_call_flow`, `trillet_delete_call_flow` (requires `confirm: true`)
|
|
69
|
+
|
|
70
|
+
**Flow settings** — `trillet_set_transfers` (warm/cold/whisper/agent, condition gates, business-hours routing), `trillet_set_office_hours`, `trillet_set_call_settings` (voicemail detection, IVR navigation, silence, duration, recording), `trillet_set_speech_settings`, `trillet_set_welcome_message`, `trillet_set_code_actions` (feature-gated Python actions; detects silent strips)
|
|
71
|
+
|
|
72
|
+
**Node flows** — `trillet_node_flow_reference` (the full node-type/edge/validation reference the model reads before building), `trillet_list_node_flows`, `trillet_get_node_flow`, `trillet_create_node_flow`, `trillet_list_node_flow_templates`, `trillet_update_node_graph`, `trillet_validate_node_graph`, `trillet_publish_node_graph`, `trillet_simulate_node_graph`, `trillet_list_node_flow_runs`
|
|
73
|
+
|
|
74
|
+
**Phone numbers** — `trillet_list_phone_numbers`, `trillet_search_available_numbers`, `trillet_purchase_phone_number` (requires `confirm: true`; bills the workspace), `trillet_bind_phone_number` (office hours, metadata tags, secret bindings), `trillet_release_phone_number` (unbind from agent), `trillet_get_phone_number_config`, `trillet_update_phone_number_config`, `trillet_register_external_number` (BYO Twilio / SIP trunks, requires `confirm: true`)
|
|
75
|
+
|
|
76
|
+
**Knowledge bases** — `trillet_list_knowledge_bases`, `trillet_get_knowledge_base` (with flow usage), `trillet_create_knowledge_base`, `trillet_add_kb_text`, `trillet_upload_kb_file`, `trillet_attach_knowledge_bases`, `trillet_delete_knowledge_base` (whole KB or one file; requires `confirm: true`)
|
|
77
|
+
|
|
78
|
+
**Workflows** — `trillet_workflow_reference` (the full trigger/step/template reference the model reads before building: 7 triggers, 15 step types), `trillet_list_workflows`, `trillet_get_workflow`, `trillet_create_workflow`, `trillet_update_workflow`, `trillet_activate_workflow`, `trillet_pause_workflow`, `trillet_delete_workflow` (requires `confirm: true`), `trillet_test_workflow` (simulated side effects, works on drafts), `trillet_run_workflow` (real run, requires `confirm: true`), `trillet_list_workflow_runs`, `trillet_get_workflow_run` (per-step logs)
|
|
79
|
+
|
|
80
|
+
**Secrets** — `trillet_list_secrets` (always masked), `trillet_set_secret`, `trillet_delete_secret`
|
|
81
|
+
|
|
82
|
+
**Calls** — `trillet_list_calls` (filters + per-call cost/warm-transfer billing fields), `trillet_get_call_metrics` (aggregates), `trillet_get_call` (transcript, analysis, billing breakdown, optional functionCalls), `trillet_start_outbound_call` (requires explicit `confirm: true`; places a real phone call)
|
|
83
|
+
|
|
84
|
+
**Account** — `trillet_get_usage` (wallet balance, optional 3-month minutes breakdown), `trillet_list_transactions` (charge ledger, month x type summaries)
|
|
85
|
+
|
|
86
|
+
## Design notes
|
|
87
|
+
|
|
88
|
+
The tool descriptions encode hard-won platform behavior so the model does the right thing first time:
|
|
89
|
+
|
|
90
|
+
- **Editor drafts override production** for dashboard test calls; `trillet_get_call_flow` reports `has_active_draft` and `trillet_deactivate_drafts` returns a flow to production. This is the most common cause of "my changes aren't taking effect".
|
|
91
|
+
- **Update merge semantics**: objects deep-merge, arrays replace wholesale, nulls are ignored. The dedicated setters (`trillet_set_incall_api_tools`, `trillet_set_post_call_analysis`, `trillet_attach_knowledge_bases`) always send complete arrays.
|
|
92
|
+
- **In-call API tools**: the runtime appends call metadata to requests unless `callSetting.disableMetadata` is set; strict APIs (e.g. Airtable) reject unknown keys, so the setter defaults it on. Prompts should reference tools as `{{realtime_api "name" "vars"}}`.
|
|
93
|
+
- **Workflows**: `maxRunsPerContact` defaults to 1/24h platform-side (silently skips repeat callers); the create tool defaults it to 500. Web test calls fire the *outbound* call-completed trigger.
|
|
94
|
+
- **Prompts**: never script a greeting line in the prompt; the platform welcome message already plays it, and a scripted opening makes the agent greet twice.
|
|
95
|
+
|
|
96
|
+
## Roadmap to the Anthropic directory
|
|
97
|
+
|
|
98
|
+
- [x] Official SDK, stdio transport, tool annotations (`readOnlyHint`/`destructiveHint`), narrow tool descriptions, no dynamic code execution, secrets via env only
|
|
99
|
+
- [ ] Public privacy policy page (required for directory review)
|
|
100
|
+
- [ ] Remote server (Streamable HTTP + OAuth) for the Claude.ai connector directory (submission requires a Team/Enterprise org)
|
|
101
|
+
- [x] `.mcpb` Desktop Extension bundle for one-click local install (`trillet.mcpb` in the plugin repo)
|
|
102
|
+
- [ ] npm publish as `@trillet-ai/mcp` for `npx` usage (commands in [PUBLISH.md](./PUBLISH.md); not published yet)
|
|
103
|
+
|
|
104
|
+
## Security
|
|
105
|
+
|
|
106
|
+
- The API key and workspace id are read from environment variables and never logged.
|
|
107
|
+
- All output goes to stdout as MCP protocol messages; diagnostics go to stderr.
|
|
108
|
+
- Tools with real-world side effects or destructive outcomes (`trillet_start_outbound_call`, `trillet_run_workflow`, `trillet_purchase_phone_number`, `trillet_register_external_number`, and every delete tool) are annotated destructive and refuse to run without `confirm: true`.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client for the Trillet public API.
|
|
3
|
+
*
|
|
4
|
+
* Auth model (verified against production):
|
|
5
|
+
* - `x-api-key`: a Trillet user API key. User keys are cross-workspace.
|
|
6
|
+
* - `x-workspace-id`: REQUIRED on every request. The key's user must be a
|
|
7
|
+
* member of the workspace or the API returns 401.
|
|
8
|
+
*/
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
const DEFAULT_BASE_URL = "https://api.trillet.ai";
|
|
11
|
+
export const SETUP_HINT = "Trillet credentials are not configured. Run the /trillet:setup command (it asks for a Trillet API key and workspace id and saves them), then restart Claude Code. Keys are created in the Trillet dashboard under Settings > API Keys.";
|
|
12
|
+
/**
|
|
13
|
+
* Never throws: a server with missing credentials must still start so tools
|
|
14
|
+
* can return an actionable setup message instead of the whole server dying.
|
|
15
|
+
*/
|
|
16
|
+
export function loadConfig() {
|
|
17
|
+
return {
|
|
18
|
+
apiKey: (process.env.TRILLET_API_KEY || "").trim(),
|
|
19
|
+
workspaceId: (process.env.TRILLET_WORKSPACE_ID || "").trim(),
|
|
20
|
+
baseUrl: process.env.TRILLET_API_URL || DEFAULT_BASE_URL,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export class TrilletError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
body;
|
|
26
|
+
constructor(status, body, method, path) {
|
|
27
|
+
// A flow/agent/call id from another workspace comes back as 404 or even a
|
|
28
|
+
// bare 500 ("Error fetching flow"), which reads like an outage. Say so.
|
|
29
|
+
const wrongWorkspaceHint = method === "GET" && (status === 404 || status === 500) && /\/[0-9a-f]{24}(\/|\?|$)/i.test(path)
|
|
30
|
+
? " (If this id belongs to a client or sub-workspace, retry with that workspace_id: ids from another workspace fail this way.)"
|
|
31
|
+
: "";
|
|
32
|
+
super(`Trillet API ${method} ${path} failed with HTTP ${status}: ${body}${wrongWorkspaceHint}`);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.body = body;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class TrilletClient {
|
|
38
|
+
cfg;
|
|
39
|
+
constructor(cfg) {
|
|
40
|
+
this.cfg = cfg;
|
|
41
|
+
}
|
|
42
|
+
/** Base API URL (text-call tools map it to the environment's LiveKit host). */
|
|
43
|
+
get baseUrl() {
|
|
44
|
+
return this.cfg.baseUrl;
|
|
45
|
+
}
|
|
46
|
+
/** Default workspace id (text-call caps are scoped per workspace). */
|
|
47
|
+
get workspaceId() {
|
|
48
|
+
return this.cfg.workspaceId;
|
|
49
|
+
}
|
|
50
|
+
/** Where writes land: "production" unless the embedder asked for "draft". */
|
|
51
|
+
get writeTarget() {
|
|
52
|
+
return this.cfg.writeTarget === "draft" ? "draft" : "production";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Perform a request. `workspaceId` overrides the default workspace for this
|
|
56
|
+
* call only (agency accounts manage several workspaces with one key).
|
|
57
|
+
*/
|
|
58
|
+
async request(method, path, opts = {}) {
|
|
59
|
+
if (!this.cfg.apiKey || !(opts.workspaceId || this.cfg.workspaceId)) {
|
|
60
|
+
throw new Error(SETUP_HINT);
|
|
61
|
+
}
|
|
62
|
+
const url = new URL(path, this.cfg.baseUrl);
|
|
63
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
64
|
+
if (v !== undefined)
|
|
65
|
+
url.searchParams.set(k, String(v));
|
|
66
|
+
}
|
|
67
|
+
const headers = {
|
|
68
|
+
"x-api-key": this.cfg.apiKey,
|
|
69
|
+
"x-workspace-id": opts.workspaceId || this.cfg.workspaceId,
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
"user-agent": `trillet-mcp/${VERSION}`,
|
|
72
|
+
};
|
|
73
|
+
const isRead = method === "GET" || method === "DELETE";
|
|
74
|
+
if (!isRead && this.writeTarget === "draft") {
|
|
75
|
+
headers["x-trillet-write-target"] = "draft";
|
|
76
|
+
}
|
|
77
|
+
const res = await fetch(url, {
|
|
78
|
+
method,
|
|
79
|
+
headers,
|
|
80
|
+
// Several Trillet POST endpoints reject an empty body when
|
|
81
|
+
// content-type is application/json, so default to {}.
|
|
82
|
+
body: isRead ? undefined : JSON.stringify(opts.body ?? {}),
|
|
83
|
+
});
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new TrilletError(res.status, text.slice(0, 1000), method, path);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Multipart upload (knowledge base files). */
|
|
96
|
+
async upload(path, fieldName, filename, data, mimetype, workspaceId) {
|
|
97
|
+
if (!this.cfg.apiKey || !(workspaceId || this.cfg.workspaceId)) {
|
|
98
|
+
throw new Error(SETUP_HINT);
|
|
99
|
+
}
|
|
100
|
+
const form = new FormData();
|
|
101
|
+
form.append(fieldName, new Blob([data], { type: mimetype }), filename);
|
|
102
|
+
const res = await fetch(new URL(path, this.cfg.baseUrl), {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"x-api-key": this.cfg.apiKey,
|
|
106
|
+
"x-workspace-id": workspaceId || this.cfg.workspaceId,
|
|
107
|
+
},
|
|
108
|
+
body: form,
|
|
109
|
+
});
|
|
110
|
+
const text = await res.text();
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new TrilletError(res.status, text.slice(0, 1000), "POST", path);
|
|
113
|
+
}
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TrilletError } from "./client.js";
|
|
3
|
+
/** Standard success payload: pretty JSON in a text block. */
|
|
4
|
+
export function ok(data) {
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Standard error payload with actionable messages for common failures. */
|
|
15
|
+
export function fail(err) {
|
|
16
|
+
let msg = err instanceof Error ? err.message : String(err);
|
|
17
|
+
if (err instanceof TrilletError) {
|
|
18
|
+
if (err.status === 401) {
|
|
19
|
+
msg +=
|
|
20
|
+
"\nHint: the API key's user is not a member of this workspace. Check TRILLET_WORKSPACE_ID or the workspace_id argument.";
|
|
21
|
+
}
|
|
22
|
+
else if (err.status === 400 && err.body.includes("Workspace ID")) {
|
|
23
|
+
msg += "\nHint: x-workspace-id header missing; set TRILLET_WORKSPACE_ID.";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Wrap a handler so thrown errors become MCP error results, never crashes. */
|
|
32
|
+
export function guard(fn) {
|
|
33
|
+
return async (...args) => {
|
|
34
|
+
try {
|
|
35
|
+
return await fn(...args);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return fail(e);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Optional per-call workspace override, shared by every tool. */
|
|
43
|
+
export const workspaceParam = {
|
|
44
|
+
workspace_id: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Override the default workspace for this call (24-char hex id). Omit to use TRILLET_WORKSPACE_ID. Useful for agency accounts managing client workspaces."),
|
|
48
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Trillet AI MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Exposes the Trillet public API as MCP tools for building and operating
|
|
6
|
+
* voice agents: agents, call flows (prompts, in-call API tools, post-call
|
|
7
|
+
* analysis, drafts), knowledge bases, workflows, and call history.
|
|
8
|
+
*
|
|
9
|
+
* Configuration (environment variables):
|
|
10
|
+
* TRILLET_API_KEY required, a Trillet user API key
|
|
11
|
+
* TRILLET_WORKSPACE_ID required, default workspace id
|
|
12
|
+
* TRILLET_API_URL optional, defaults to https://api.trillet.ai
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { loadConfig, TrilletClient } from "./client.js";
|
|
17
|
+
import { registerAgentTools } from "./tools/agents.js";
|
|
18
|
+
import { registerCallFlowTools } from "./tools/callFlows.js";
|
|
19
|
+
import { registerKnowledgeBaseTools } from "./tools/knowledgeBases.js";
|
|
20
|
+
import { registerWorkflowTools } from "./tools/workflows.js";
|
|
21
|
+
import { registerCallTools } from "./tools/calls.js";
|
|
22
|
+
import { registerNodeFlowTools } from "./tools/nodeFlows.js";
|
|
23
|
+
import { registerPhoneNumberTools } from "./tools/phoneNumbers.js";
|
|
24
|
+
import { registerFlowSettingTools } from "./tools/flowSettings.js";
|
|
25
|
+
import { registerSecretTools } from "./tools/secrets.js";
|
|
26
|
+
import { registerAccountTools } from "./tools/account.js";
|
|
27
|
+
import { registerTextCallTools } from "./tools/textCalls.js";
|
|
28
|
+
import { runTextCallWorker } from "./textcall/worker.js";
|
|
29
|
+
import { VERSION } from "./version.js";
|
|
30
|
+
async function main() {
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
const client = new TrilletClient(config);
|
|
33
|
+
const server = new McpServer({
|
|
34
|
+
name: "trillet",
|
|
35
|
+
version: VERSION,
|
|
36
|
+
});
|
|
37
|
+
registerAgentTools(server, client);
|
|
38
|
+
registerCallFlowTools(server, client);
|
|
39
|
+
registerFlowSettingTools(server, client);
|
|
40
|
+
registerNodeFlowTools(server, client);
|
|
41
|
+
registerPhoneNumberTools(server, client);
|
|
42
|
+
registerKnowledgeBaseTools(server, client);
|
|
43
|
+
registerWorkflowTools(server, client);
|
|
44
|
+
registerSecretTools(server, client);
|
|
45
|
+
registerCallTools(server, client);
|
|
46
|
+
registerTextCallTools(server, client);
|
|
47
|
+
registerAccountTools(server, client);
|
|
48
|
+
const transport = new StdioServerTransport();
|
|
49
|
+
await server.connect(transport);
|
|
50
|
+
// stdout is the MCP transport; all logging must go to stderr.
|
|
51
|
+
console.error(`trillet-mcp v${VERSION} ready (workspace ${config.workspaceId}, api ${config.baseUrl})`);
|
|
52
|
+
}
|
|
53
|
+
// Text-call worker mode: this same file is forked as the LiveKit room
|
|
54
|
+
// participant for text test calls (see textcall/worker.ts for why it must be
|
|
55
|
+
// a separate process). Everything else — including the MCP server — stays off.
|
|
56
|
+
if (process.env.TRILLET_MCP_TEXTCALL_WORKER === "1") {
|
|
57
|
+
runTextCallWorker();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
console.error("trillet-mcp fatal:", err?.message ?? err);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic tool registry.
|
|
3
|
+
*
|
|
4
|
+
* The same 76 tools power three surfaces: the stdio MCP server (Claude Code
|
|
5
|
+
* plugin + Desktop .mcpb), a future remote MCP, and the in-dashboard AI
|
|
6
|
+
* assistant (trillet-backend embeds the bundled build of this module and runs
|
|
7
|
+
* the tools inside its own Anthropic loop).
|
|
8
|
+
*
|
|
9
|
+
* Collection works by passing a fake McpServer into the existing
|
|
10
|
+
* register*Tools() functions, so the tool files stay exactly as written for
|
|
11
|
+
* the MCP SDK — no churn, no drift between surfaces. Handlers close over the
|
|
12
|
+
* TrilletClient passed to buildRegistry(), so embedders build one registry
|
|
13
|
+
* per authenticated session (cheap: 76 object literals).
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
17
|
+
import { TrilletClient } from "./client.js";
|
|
18
|
+
import { VERSION } from "./version.js";
|
|
19
|
+
import { registerAgentTools } from "./tools/agents.js";
|
|
20
|
+
import { registerCallFlowTools } from "./tools/callFlows.js";
|
|
21
|
+
import { registerKnowledgeBaseTools } from "./tools/knowledgeBases.js";
|
|
22
|
+
import { registerWorkflowTools } from "./tools/workflows.js";
|
|
23
|
+
import { registerCallTools } from "./tools/calls.js";
|
|
24
|
+
import { registerNodeFlowTools } from "./tools/nodeFlows.js";
|
|
25
|
+
import { registerPhoneNumberTools } from "./tools/phoneNumbers.js";
|
|
26
|
+
import { registerFlowSettingTools } from "./tools/flowSettings.js";
|
|
27
|
+
import { registerSecretTools } from "./tools/secrets.js";
|
|
28
|
+
import { registerAccountTools } from "./tools/account.js";
|
|
29
|
+
import { registerTextCallTools } from "./tools/textCalls.js";
|
|
30
|
+
const REGISTER_FNS = [
|
|
31
|
+
registerAgentTools,
|
|
32
|
+
registerCallFlowTools,
|
|
33
|
+
registerFlowSettingTools,
|
|
34
|
+
registerNodeFlowTools,
|
|
35
|
+
registerPhoneNumberTools,
|
|
36
|
+
registerKnowledgeBaseTools,
|
|
37
|
+
registerWorkflowTools,
|
|
38
|
+
registerSecretTools,
|
|
39
|
+
registerCallTools,
|
|
40
|
+
registerTextCallTools,
|
|
41
|
+
registerAccountTools,
|
|
42
|
+
];
|
|
43
|
+
/** Collect every tool, with handlers bound to the given client. */
|
|
44
|
+
export function buildRegistry(client) {
|
|
45
|
+
const defs = [];
|
|
46
|
+
const collector = {
|
|
47
|
+
registerTool(name, config, handler) {
|
|
48
|
+
defs.push({
|
|
49
|
+
name,
|
|
50
|
+
title: config.title ?? name,
|
|
51
|
+
description: config.description ?? "",
|
|
52
|
+
inputSchema: config.inputSchema ?? {},
|
|
53
|
+
annotations: config.annotations ?? {},
|
|
54
|
+
handler,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
for (const fn of REGISTER_FNS)
|
|
59
|
+
fn(collector, client);
|
|
60
|
+
return defs;
|
|
61
|
+
}
|
|
62
|
+
/** One tool's JSON-schema form, e.g. for the Anthropic Messages API tools[]. */
|
|
63
|
+
export function toJsonSchemaTool(def, opts = {}) {
|
|
64
|
+
const shape = { ...def.inputSchema };
|
|
65
|
+
for (const key of opts.stripArgs ?? [])
|
|
66
|
+
delete shape[key];
|
|
67
|
+
const schema = zodToJsonSchema(z.object(shape), { target: "jsonSchema7" });
|
|
68
|
+
delete schema.$schema;
|
|
69
|
+
const text = opts.transformText ?? ((t) => t);
|
|
70
|
+
return {
|
|
71
|
+
name: def.name,
|
|
72
|
+
description: text(def.description),
|
|
73
|
+
input_schema: schema,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Execute a tool by def, honoring the same stripArgs contract as
|
|
78
|
+
* toJsonSchemaTool. Returns { text, isError } — the text is the JSON (or
|
|
79
|
+
* error message) the tool produced, ready to use as a tool_result body.
|
|
80
|
+
*/
|
|
81
|
+
export async function executeTool(def, args, opts = {}) {
|
|
82
|
+
const cleaned = { ...args };
|
|
83
|
+
for (const key of opts.stripArgs ?? [])
|
|
84
|
+
delete cleaned[key];
|
|
85
|
+
// Mirror the MCP SDK: zod-parse before the handler runs, so defaults apply
|
|
86
|
+
// (e.g. stub_incall_apis=true) and bad arg types come back as a retryable
|
|
87
|
+
// error message instead of a handler crash. Unknown keys are stripped.
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = z.object(def.inputSchema).parse(cleaned);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
const detail = e?.errors?.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
94
|
+
return { text: `Invalid arguments for ${def.name}: ${detail || e?.message || e}`, isError: true };
|
|
95
|
+
}
|
|
96
|
+
const res = await def.handler(parsed);
|
|
97
|
+
return {
|
|
98
|
+
text: res.content?.map((c) => c.text).join("\n") ?? "",
|
|
99
|
+
isError: res.isError === true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Embedders fork text-call workers from their own entry script (see
|
|
103
|
+
// TRILLET_MCP_TEXTCALL_WORKER_PATH in tools/textCalls.ts); that script just
|
|
104
|
+
// calls this.
|
|
105
|
+
export { runTextCallWorker } from "./textcall/worker.js";
|
|
106
|
+
export { TrilletClient, VERSION };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy installer for @livekit/rtc-node.
|
|
3
|
+
*
|
|
4
|
+
* Text test calls need a real LiveKit room participant (agent turns arrive as
|
|
5
|
+
* text streams over the WebRTC data channel — there is no HTTP ingress for
|
|
6
|
+
* them). @livekit/rtc-node carries a ~16MB native FFI binary per platform, and
|
|
7
|
+
* neither distribution of this server runs `npm install` at install time (the
|
|
8
|
+
* Claude Code plugin is a git clone with node_modules gitignored; the Desktop
|
|
9
|
+
* .mcpb is a static pack). So the dependency is installed on first use into a
|
|
10
|
+
* cache directory under the user's home, and only text-call users pay for it.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
/** Pinned: the version every live verification ran against. */
|
|
17
|
+
export const RTC_NODE_VERSION = "0.13.34";
|
|
18
|
+
export function depsDir() {
|
|
19
|
+
return (process.env.TRILLET_MCP_DEPS_DIR ||
|
|
20
|
+
join(os.homedir(), ".trillet-mcp", "textcall-deps"));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The package blocks subpath exports (requiring
|
|
24
|
+
* '@livekit/rtc-node/package.json' throws ERR_PACKAGE_PATH_NOT_EXPORTED), so
|
|
25
|
+
* the installed check reads the manifest straight off disk.
|
|
26
|
+
*/
|
|
27
|
+
export function rtcNodeInstalled() {
|
|
28
|
+
try {
|
|
29
|
+
const manifest = join(depsDir(), "node_modules", "@livekit", "rtc-node", "package.json");
|
|
30
|
+
if (!existsSync(manifest))
|
|
31
|
+
return false;
|
|
32
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
33
|
+
return pkg.version === RTC_NODE_VERSION;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Install (or upgrade to) the pinned rtc-node into the cache dir via npm. */
|
|
40
|
+
export async function ensureRtcNode() {
|
|
41
|
+
if (rtcNodeInstalled())
|
|
42
|
+
return;
|
|
43
|
+
const dir = depsDir();
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({
|
|
46
|
+
name: "trillet-mcp-textcall-deps",
|
|
47
|
+
private: true,
|
|
48
|
+
description: "Native LiveKit client used by trillet-mcp text test calls. Safe to delete; reinstalled on next use.",
|
|
49
|
+
dependencies: { "@livekit/rtc-node": RTC_NODE_VERSION },
|
|
50
|
+
}, null, 2));
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
53
|
+
const child = spawn(npm, ["install", "--no-audit", "--no-fund", "--loglevel=error"], {
|
|
54
|
+
cwd: dir,
|
|
55
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
56
|
+
shell: process.platform === "win32",
|
|
57
|
+
});
|
|
58
|
+
let stderr = "";
|
|
59
|
+
child.stderr?.on("data", (d) => {
|
|
60
|
+
stderr += String(d);
|
|
61
|
+
});
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
child.kill();
|
|
64
|
+
reject(new Error("npm install of @livekit/rtc-node timed out after 240s"));
|
|
65
|
+
}, 240_000);
|
|
66
|
+
child.on("error", (err) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(new Error(`Could not run npm to install the text-call dependency (@livekit/rtc-node): ${err.message}. ` +
|
|
69
|
+
`Install Node.js with npm, or manually run: cd ${dir} && npm install`));
|
|
70
|
+
});
|
|
71
|
+
child.on("exit", (code) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
if (code === 0 && rtcNodeInstalled())
|
|
74
|
+
resolve();
|
|
75
|
+
else
|
|
76
|
+
reject(new Error(`npm install of @livekit/rtc-node failed (exit ${code}). ${stderr.slice(0, 500)}\n` +
|
|
77
|
+
`Manual fix: cd ${dir} && npm install`));
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|