thalixtower-mcp 0.6.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/README.md +43 -0
- package/dist/index.js +208 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# thalixtower-mcp
|
|
2
|
+
|
|
3
|
+
Thalix Tower MCP server — exposes `atc_*` tools to MCP-capable agents (Claude Code),
|
|
4
|
+
so an agent coordinates natively instead of running shell commands.
|
|
5
|
+
|
|
6
|
+
## Configure
|
|
7
|
+
|
|
8
|
+
Get a **frequency token** from the dashboard (https://tower.thalixinc.ai), then add
|
|
9
|
+
to your project's `.mcp.json` (or Claude settings) — or run `atc init` from
|
|
10
|
+
`thalixtower-cli` to scaffold it:
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"agent-tower": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "thalixtower-mcp"],
|
|
18
|
+
"env": { "ATC_TOKEN": "atcf_…" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`ATC_API` is optional (defaults to the hosted prod API; set it to target the dev
|
|
25
|
+
stack).
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
`atc_checkin` · `atc_brief` · `atc_standing` · `atc_standing_propose` ·
|
|
30
|
+
`atc_squawk` · `atc_claim` · `atc_clear` · `atc_note` · `atc_notes` ·
|
|
31
|
+
`atc_checkout`.
|
|
32
|
+
|
|
33
|
+
The checkin brief opens with the agent's **standing orders** — per-project
|
|
34
|
+
instructions assigned via the token's alignment (humans manage these in the
|
|
35
|
+
dashboard or with `atc login`). `atc_standing_propose` lets the agent suggest a
|
|
36
|
+
draft order; a human reviews, activates, and assigns it.
|
|
37
|
+
|
|
38
|
+
The server shares one session per workspace with the CLI via `.atc/session.json`
|
|
39
|
+
(gitignore `.atc/`), so the CLI, hooks, and MCP never invalidate each other.
|
|
40
|
+
|
|
41
|
+
Pair it with the coordination block in your `CLAUDE.md`/`AGENTS.md` (see the
|
|
42
|
+
project's `templates/agent-tower.md`) so the agent knows to use these at the
|
|
43
|
+
right moments.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var API = (process.env.ATC_API || "https://api.tower.thalixinc.ai").replace(/\/$/, "");
|
|
11
|
+
var TOKEN = process.env.ATC_TOKEN ?? "";
|
|
12
|
+
var ATC_DIR = path.join(process.cwd(), ".atc");
|
|
13
|
+
var SESSION_FILE = path.join(ATC_DIR, "session.json");
|
|
14
|
+
var session = null;
|
|
15
|
+
function readSessionFile() {
|
|
16
|
+
try {
|
|
17
|
+
const s = JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
|
|
18
|
+
if (typeof s?.sessionToken === "string" && s.sessionToken) return s;
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function writeSessionFile(s) {
|
|
24
|
+
fs.mkdirSync(ATC_DIR, { recursive: true });
|
|
25
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify({ ...s, api: API }, null, 2));
|
|
26
|
+
}
|
|
27
|
+
function clearSessionFile() {
|
|
28
|
+
try {
|
|
29
|
+
fs.rmSync(SESSION_FILE);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function workspaceId() {
|
|
34
|
+
const file = path.join(ATC_DIR, "workspace.json");
|
|
35
|
+
try {
|
|
36
|
+
const existing = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
37
|
+
if (existing.workspaceId) return existing.workspaceId;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
const id = `ws_${randomUUID()}`;
|
|
41
|
+
fs.mkdirSync(ATC_DIR, { recursive: true });
|
|
42
|
+
fs.writeFileSync(file, JSON.stringify({ workspaceId: id }, null, 2));
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
async function call(bearer, method, apiPath, body) {
|
|
46
|
+
const res = await fetch(`${API}${apiPath}`, {
|
|
47
|
+
method,
|
|
48
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
49
|
+
body: body ? JSON.stringify(body) : void 0
|
|
50
|
+
});
|
|
51
|
+
const text2 = await res.text();
|
|
52
|
+
let data = {};
|
|
53
|
+
try {
|
|
54
|
+
data = text2 ? JSON.parse(text2) : {};
|
|
55
|
+
} catch {
|
|
56
|
+
data = { error: text2 };
|
|
57
|
+
}
|
|
58
|
+
return { ok: res.ok, status: res.status, data };
|
|
59
|
+
}
|
|
60
|
+
var text = (data, isError = false) => ({
|
|
61
|
+
content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }],
|
|
62
|
+
isError
|
|
63
|
+
});
|
|
64
|
+
var needConfig = () => !API || !TOKEN ? text("ATC_API and ATC_TOKEN must be set in the MCP server env", true) : null;
|
|
65
|
+
async function sessionCall(method, apiPath, body) {
|
|
66
|
+
if (!session) {
|
|
67
|
+
const shared = readSessionFile();
|
|
68
|
+
if (shared) session = shared;
|
|
69
|
+
}
|
|
70
|
+
if (!session) return null;
|
|
71
|
+
let r = await call(session.sessionToken, method, apiPath, body);
|
|
72
|
+
if (r.status === 401) {
|
|
73
|
+
const shared = readSessionFile();
|
|
74
|
+
if (shared && shared.sessionToken !== session.sessionToken) {
|
|
75
|
+
r = await call(shared.sessionToken, method, apiPath, body);
|
|
76
|
+
if (r.ok) session = shared;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return r;
|
|
80
|
+
}
|
|
81
|
+
var sessionResult = (r) => r ? text(r.data, !r.ok) : text("not checked in \u2014 call atc_checkin first", true);
|
|
82
|
+
var server = new McpServer({ name: "thalix-tower", version: "0.6.0" });
|
|
83
|
+
server.registerTool(
|
|
84
|
+
"atc_checkin",
|
|
85
|
+
{
|
|
86
|
+
description: "Contact the Tower: declare identity + task, get your callsign and a brief. If the brief carries standing orders, read and follow them.",
|
|
87
|
+
inputSchema: { task: z.string().optional(), callsign: z.string().optional(), cli: z.string().optional() }
|
|
88
|
+
},
|
|
89
|
+
async (args) => {
|
|
90
|
+
const cfg = needConfig();
|
|
91
|
+
if (cfg) return cfg;
|
|
92
|
+
const r = await call(TOKEN, "POST", "/v1/sessions", {
|
|
93
|
+
workspaceId: workspaceId(),
|
|
94
|
+
callsign: args.callsign,
|
|
95
|
+
cli: args.cli ?? "claude",
|
|
96
|
+
worktree: process.cwd(),
|
|
97
|
+
task: args.task
|
|
98
|
+
});
|
|
99
|
+
if (!r.ok) return text(r.data, true);
|
|
100
|
+
session = { callsign: r.data.callsign, sessionToken: r.data.sessionToken };
|
|
101
|
+
writeSessionFile(session);
|
|
102
|
+
return text({ callsign: r.data.callsign, brief: r.data.brief });
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
server.registerTool(
|
|
106
|
+
"atc_brief",
|
|
107
|
+
{ description: "Orient: standing orders + roster + claims + conflicts touching you + NOTAMs + traffic (unacked inbox count + preview).", inputSchema: {} },
|
|
108
|
+
async () => sessionResult(await sessionCall("GET", "/v1/brief"))
|
|
109
|
+
);
|
|
110
|
+
server.registerTool(
|
|
111
|
+
"atc_standing",
|
|
112
|
+
{
|
|
113
|
+
description: "The frequency's standing orders in full (project-wide instructions every session must follow; briefs truncate long ones).",
|
|
114
|
+
inputSchema: {}
|
|
115
|
+
},
|
|
116
|
+
async () => sessionResult(await sessionCall("GET", "/v1/standing"))
|
|
117
|
+
);
|
|
118
|
+
server.registerTool(
|
|
119
|
+
"atc_squawk",
|
|
120
|
+
{
|
|
121
|
+
description: "Broadcast status (also the heartbeat). Returns conflicts touching your claims + unacked traffic count.",
|
|
122
|
+
inputSchema: { status: z.string().optional(), code: z.enum(["working", "blocked", "review", "mayday"]).optional() }
|
|
123
|
+
},
|
|
124
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/squawk", args))
|
|
125
|
+
);
|
|
126
|
+
server.registerTool(
|
|
127
|
+
"atc_claim",
|
|
128
|
+
{
|
|
129
|
+
description: "Request clearance on a path glob. Returns conflicts INLINE \u2014 if non-empty, do NOT edit; squawk blocked and tell the human.",
|
|
130
|
+
inputSchema: { glob: z.string() }
|
|
131
|
+
},
|
|
132
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/claims", { glob: args.glob }))
|
|
133
|
+
);
|
|
134
|
+
server.registerTool(
|
|
135
|
+
"atc_clear",
|
|
136
|
+
{ description: "Release a claim (or all of yours if glob omitted).", inputSchema: { glob: z.string().optional() } },
|
|
137
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/clear", { glob: args.glob }))
|
|
138
|
+
);
|
|
139
|
+
server.registerTool(
|
|
140
|
+
"atc_note",
|
|
141
|
+
{
|
|
142
|
+
description: "Write a NOTAM to the durable decision log. supersedes retires an older NOTAM.",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
body: z.string(),
|
|
145
|
+
tags: z.array(z.string()).optional(),
|
|
146
|
+
pinned: z.boolean().optional(),
|
|
147
|
+
supersedes: z.string().optional()
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/notams", args))
|
|
151
|
+
);
|
|
152
|
+
server.registerTool(
|
|
153
|
+
"atc_notes",
|
|
154
|
+
{
|
|
155
|
+
description: "List the decision log (NOTAMs), optionally filtered by tag \u2014 or pass id for one full NOTAM body (briefs truncate).",
|
|
156
|
+
inputSchema: { tag: z.string().optional(), id: z.string().optional() }
|
|
157
|
+
},
|
|
158
|
+
async (args) => {
|
|
159
|
+
if (args.id) {
|
|
160
|
+
return sessionResult(await sessionCall("GET", `/v1/notams/${encodeURIComponent(args.id)}`));
|
|
161
|
+
}
|
|
162
|
+
const q = args.tag ? `?tag=${encodeURIComponent(args.tag)}` : "";
|
|
163
|
+
return sessionResult(await sessionCall("GET", `/v1/notams${q}`));
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
server.registerTool(
|
|
167
|
+
"atc_checkout",
|
|
168
|
+
{ description: "Leave cleanly: release claims and drop off the board.", inputSchema: {} },
|
|
169
|
+
async () => {
|
|
170
|
+
const r = await sessionCall("POST", "/v1/checkout");
|
|
171
|
+
session = null;
|
|
172
|
+
clearSessionFile();
|
|
173
|
+
return sessionResult(r);
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
server.registerTool(
|
|
177
|
+
"atc_call",
|
|
178
|
+
{
|
|
179
|
+
description: "Call another agent on this frequency (or '*' for everyone): writes to their durable inbox; they receive it at their next checkpoint. Mail to offline callsigns queues.",
|
|
180
|
+
inputSchema: { to: z.string(), body: z.string() }
|
|
181
|
+
},
|
|
182
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/calls", { to: args.to, body: args.body }))
|
|
183
|
+
);
|
|
184
|
+
server.registerTool(
|
|
185
|
+
"atc_traffic",
|
|
186
|
+
{
|
|
187
|
+
description: "Read your inbox (calls from other agents). Acks on read by default \u2014 answer anything that needs answering via atc_call. keepUnread:true to peek without acking.",
|
|
188
|
+
inputSchema: { keepUnread: z.boolean().optional() }
|
|
189
|
+
},
|
|
190
|
+
async (args) => {
|
|
191
|
+
const r = await sessionCall("GET", "/v1/traffic");
|
|
192
|
+
if (!r) return text("not checked in \u2014 call atc_checkin first", true);
|
|
193
|
+
if (!r.ok) return text(r.data, true);
|
|
194
|
+
if (args.keepUnread !== true) {
|
|
195
|
+
await sessionCall("POST", "/v1/traffic/ack", { all: true });
|
|
196
|
+
}
|
|
197
|
+
return text(r.data);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
server.registerTool(
|
|
201
|
+
"atc_standing_propose",
|
|
202
|
+
{
|
|
203
|
+
description: "Propose a standing order DRAFT for this frequency (saved to the library; a human reviews, activates, and assigns it). Use for durable instructions future sessions should receive.",
|
|
204
|
+
inputSchema: { name: z.string(), body: z.string() }
|
|
205
|
+
},
|
|
206
|
+
async (args) => sessionResult(await sessionCall("POST", "/v1/standing-orders", { name: args.name, body: args.body }))
|
|
207
|
+
);
|
|
208
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thalixtower-mcp",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Thalix Tower MCP server — exposes atc_* tools to MCP-capable agents (Claude Code).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/manateeit/thalix-tower.git",
|
|
9
|
+
"directory": "mcp"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://tower.thalixinc.ai",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent-tower",
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"coding-agents",
|
|
17
|
+
"coordination",
|
|
18
|
+
"claude"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"agent-tower-mcp": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --packages=external",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.16.10",
|
|
45
|
+
"esbuild": "^0.24.0",
|
|
46
|
+
"typescript": "^5.6.2"
|
|
47
|
+
}
|
|
48
|
+
}
|