rhythmic-mcp 0.1.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 +94 -0
- package/dist/index.js +221 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# rhythmic-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes Rhythmic to coding agents
|
|
4
|
+
(Claude Code, Cursor, Claude Desktop, …) as typed tools. It's a thin wrapper over the
|
|
5
|
+
authenticated `/api/v1` HTTP API — all auth, tenant-scoping, and validation live there.
|
|
6
|
+
|
|
7
|
+
> Rhythmic is in **free public beta**.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
1. In Rhythmic, open **Settings → API keys** and create a key (the secret is shown once).
|
|
12
|
+
2. Register the server with your agent using `npx` — no install or build step needed.
|
|
13
|
+
|
|
14
|
+
The key can be passed as a `--api-key` flag (recommended for `mcpServers` JSON) or via the
|
|
15
|
+
`RHYTHMIC_API_KEY` environment variable. A flag wins over the env var.
|
|
16
|
+
|
|
17
|
+
### Claude Desktop / Cursor (`mcpServers` JSON)
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"rhythmic": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "rhythmic-mcp", "--api-key", "rhy_xxxxx"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Claude Code
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
claude mcp add rhythmic -- npx -y rhythmic-mcp --api-key rhy_xxxxx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or keep the key out of the command with an env var:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
claude mcp add rhythmic -e RHYTHMIC_API_KEY=rhy_xxxxx -- npx -y rhythmic-mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
| Flag | Env var | Default | |
|
|
45
|
+
|------|---------|---------|--|
|
|
46
|
+
| `--api-key`, `-k` | `RHYTHMIC_API_KEY` | — | API key from Settings → API keys (required) |
|
|
47
|
+
| `--url`, `--api-url` | `RHYTHMIC_API_URL` | `https://rhythmicapp.dev` | Rhythmic base URL |
|
|
48
|
+
| `--help`, `-h` | | | Print usage |
|
|
49
|
+
| `--version`, `-v` | | | Print version |
|
|
50
|
+
|
|
51
|
+
Pointing at a local dev server? Use `--url http://localhost:3000`.
|
|
52
|
+
|
|
53
|
+
## Local development
|
|
54
|
+
|
|
55
|
+
From the monorepo, run against `src` without building:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm --filter rhythmic-mcp dev -- --api-key rhy_xxxxx --url http://localhost:3000
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or build and run the compiled output:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm --filter rhythmic-mcp build
|
|
65
|
+
node packages/mcp/dist/index.js --api-key rhy_xxxxx --url http://localhost:3000
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
|------|-------------|
|
|
72
|
+
| `list_projects` | List projects in the workspace |
|
|
73
|
+
| `get_project` | Project (by key) + its cycles |
|
|
74
|
+
| `list_issues` | Issues, filtered by project/status/assignee/cycle (paginated) |
|
|
75
|
+
| `get_issue` | Full issue detail incl. comments + dependency edges |
|
|
76
|
+
| `create_issue` | Create an issue |
|
|
77
|
+
| `update_issue` | Update status/priority/title/description/assignee/cycle |
|
|
78
|
+
| `comment_issue` | Comment on an issue (as the key's user) |
|
|
79
|
+
| `set_work_status` | Toggle the live "agent is working" spinner on an issue |
|
|
80
|
+
| `link_issues` | Create a dependency (source blocks target) |
|
|
81
|
+
| `list_cycles` | Cycles with progress |
|
|
82
|
+
| `list_members` | Workspace members (resolve assignees) |
|
|
83
|
+
|
|
84
|
+
Every tool acts as the user who created the API key, scoped to that user's workspace.
|
|
85
|
+
|
|
86
|
+
## Publishing
|
|
87
|
+
|
|
88
|
+
The package is configured for public npm publishing (`publishConfig.access: public`,
|
|
89
|
+
`prepublishOnly` builds `dist`). To release:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd packages/mcp
|
|
93
|
+
npm publish
|
|
94
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
// Rhythmic MCP server — exposes the /api/v1 surface to coding agents as typed tools.
|
|
7
|
+
// Configure with `--api-key` / `--url` flags or the RHYTHMIC_API_KEY / RHYTHMIC_API_URL
|
|
8
|
+
// env vars (flags win). Create a key in Rhythmic under Settings → API keys.
|
|
9
|
+
//
|
|
10
|
+
// npx rhythmic-mcp --api-key rhy_xxx
|
|
11
|
+
const VERSION = createRequire(import.meta.url)("../package.json")
|
|
12
|
+
.version;
|
|
13
|
+
// Minimal flag parser: supports `--flag value` and `--flag=value` (and -k for the key).
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
const eq = arg.indexOf("=");
|
|
19
|
+
const [flag, inlineVal] = eq === -1 ? [arg, undefined] : [arg.slice(0, eq), arg.slice(eq + 1)];
|
|
20
|
+
const take = () => inlineVal ?? argv[++i];
|
|
21
|
+
if (flag === "--api-key" || flag === "-k")
|
|
22
|
+
out.apiKey = take();
|
|
23
|
+
else if (flag === "--url" || flag === "--api-url")
|
|
24
|
+
out.url = take();
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
const argv = process.argv.slice(2);
|
|
29
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
30
|
+
console.log(`rhythmic-mcp v${VERSION} — MCP server for Rhythmic\n\n` +
|
|
31
|
+
`Usage: rhythmic-mcp [--api-key <key>] [--url <baseUrl>]\n\n` +
|
|
32
|
+
`Options:\n` +
|
|
33
|
+
` -k, --api-key <key> Rhythmic API key (or set RHYTHMIC_API_KEY)\n` +
|
|
34
|
+
` --url <baseUrl> Rhythmic base URL (or set RHYTHMIC_API_URL;\n` +
|
|
35
|
+
` default https://rhythmicapp.dev)\n` +
|
|
36
|
+
` -h, --help Show this help\n` +
|
|
37
|
+
` -v, --version Show version`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
41
|
+
console.log(VERSION);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
const flags = parseArgs(argv);
|
|
45
|
+
const BASE = (flags.url ?? process.env.RHYTHMIC_API_URL ?? "https://rhythmicapp.dev").replace(/\/$/, "");
|
|
46
|
+
const KEY = flags.apiKey ?? process.env.RHYTHMIC_API_KEY ?? "";
|
|
47
|
+
async function api(path, init = {}) {
|
|
48
|
+
const res = await fetch(`${BASE}/api/v1${path}`, {
|
|
49
|
+
method: init.method ?? "GET",
|
|
50
|
+
headers: { "x-api-key": KEY, "content-type": "application/json" },
|
|
51
|
+
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
52
|
+
});
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
|
|
56
|
+
return text ? JSON.parse(text) : null;
|
|
57
|
+
}
|
|
58
|
+
function qs(args) {
|
|
59
|
+
const p = new URLSearchParams();
|
|
60
|
+
for (const [k, v] of Object.entries(args)) {
|
|
61
|
+
if (v !== undefined && v !== null && v !== "")
|
|
62
|
+
p.set(k, String(v));
|
|
63
|
+
}
|
|
64
|
+
const s = p.toString();
|
|
65
|
+
return s ? `?${s}` : "";
|
|
66
|
+
}
|
|
67
|
+
const ok = (data) => ({
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
69
|
+
});
|
|
70
|
+
const fail = (e) => ({
|
|
71
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
});
|
|
74
|
+
const server = new McpServer({ name: "rhythmic", version: VERSION });
|
|
75
|
+
server.tool("list_projects", "List all projects in the workspace.", {}, async () => {
|
|
76
|
+
try {
|
|
77
|
+
return ok(await api("/projects"));
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return fail(e);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool("get_project", "Get a project by key (e.g. AUTH) with its cycles.", { key: z.string().describe("Project key or id") }, async ({ key }) => {
|
|
84
|
+
try {
|
|
85
|
+
return ok(await api(`/projects/${encodeURIComponent(key)}`));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return fail(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.tool("list_issues", "List issues, optionally filtered by project/status/assignee/cycle. Paginated.", {
|
|
92
|
+
project: z.string().optional().describe("Project key or id"),
|
|
93
|
+
status: z
|
|
94
|
+
.enum(["backlog", "todo", "in_progress", "done", "canceled"])
|
|
95
|
+
.optional(),
|
|
96
|
+
assignee: z.string().optional().describe("Assignee user id"),
|
|
97
|
+
cycle: z.string().optional().describe("Cycle id"),
|
|
98
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
99
|
+
offset: z.number().int().min(0).optional(),
|
|
100
|
+
}, async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
return ok(await api(`/issues${qs(args)}`));
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return fail(e);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
server.tool("get_issue", "Get full detail for an issue by key (e.g. AUTH-12): fields, comments, and dependency edges.", { key: z.string().describe("Issue key or id") }, async ({ key }) => {
|
|
109
|
+
try {
|
|
110
|
+
return ok(await api(`/issues/${encodeURIComponent(key)}`));
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
return fail(e);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
server.tool("create_issue", "Create a new issue in a project.", {
|
|
117
|
+
project: z.string().describe("Project key or id"),
|
|
118
|
+
title: z.string(),
|
|
119
|
+
description: z.string().optional(),
|
|
120
|
+
status: z
|
|
121
|
+
.enum(["backlog", "todo", "in_progress", "done", "canceled"])
|
|
122
|
+
.optional(),
|
|
123
|
+
priority: z.enum(["none", "low", "medium", "high", "urgent"]).optional(),
|
|
124
|
+
assigneeId: z.string().optional(),
|
|
125
|
+
cycleId: z.string().optional(),
|
|
126
|
+
}, async (args) => {
|
|
127
|
+
try {
|
|
128
|
+
return ok(await api("/issues", { method: "POST", body: args }));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return fail(e);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
server.tool("update_issue", "Update fields on an issue (status, priority, title, description, assignee, cycle).", {
|
|
135
|
+
key: z.string().describe("Issue key or id"),
|
|
136
|
+
status: z
|
|
137
|
+
.enum(["backlog", "todo", "in_progress", "done", "canceled"])
|
|
138
|
+
.optional(),
|
|
139
|
+
priority: z.enum(["none", "low", "medium", "high", "urgent"]).optional(),
|
|
140
|
+
title: z.string().optional(),
|
|
141
|
+
description: z.string().optional(),
|
|
142
|
+
assigneeId: z.string().nullable().optional(),
|
|
143
|
+
cycleId: z.string().nullable().optional(),
|
|
144
|
+
}, async ({ key, ...patch }) => {
|
|
145
|
+
try {
|
|
146
|
+
return ok(await api(`/issues/${encodeURIComponent(key)}`, {
|
|
147
|
+
method: "PATCH",
|
|
148
|
+
body: patch,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return fail(e);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
server.tool("comment_issue", "Add a comment to an issue (authored by the API key's user).", { key: z.string().describe("Issue key or id"), body: z.string() }, async ({ key, body }) => {
|
|
156
|
+
try {
|
|
157
|
+
return ok(await api(`/issues/${encodeURIComponent(key)}/comments`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: { body },
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return fail(e);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.tool("set_work_status", "Show or hide the live 'agent is working' spinner on an issue. Call with working:true right " +
|
|
167
|
+
"before you start working on an issue, and working:false when you finish — your teammates " +
|
|
168
|
+
"watch it move in real time. It auto-clears if left on, and any edit/comment you make keeps " +
|
|
169
|
+
"it alive.", {
|
|
170
|
+
key: z.string().describe("Issue key or id"),
|
|
171
|
+
working: z.boolean().describe("true = start working, false = done"),
|
|
172
|
+
}, async ({ key, working }) => {
|
|
173
|
+
try {
|
|
174
|
+
return ok(await api(`/issues/${encodeURIComponent(key)}/work`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: { running: working },
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return fail(e);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
server.tool("link_issues", "Create a dependency: source blocks target (issues referenced by key).", {
|
|
184
|
+
source: z.string().describe("Blocking issue key"),
|
|
185
|
+
target: z.string().describe("Blocked issue key"),
|
|
186
|
+
type: z.enum(["blocks", "relates"]).optional(),
|
|
187
|
+
}, async (args) => {
|
|
188
|
+
try {
|
|
189
|
+
return ok(await api("/dependencies", { method: "POST", body: args }));
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
return fail(e);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
server.tool("list_cycles", "List cycles (sprints) with progress, optionally for one project.", { project: z.string().optional().describe("Project key or id") }, async (args) => {
|
|
196
|
+
try {
|
|
197
|
+
return ok(await api(`/cycles${qs(args)}`));
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
return fail(e);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
server.tool("list_members", "List workspace members (for resolving assignees).", {}, async () => {
|
|
204
|
+
try {
|
|
205
|
+
return ok(await api("/members"));
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
return fail(e);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
async function main() {
|
|
212
|
+
if (!KEY) {
|
|
213
|
+
console.error("No API key set — pass --api-key <key> or set RHYTHMIC_API_KEY. Tool calls will fail with 401.");
|
|
214
|
+
}
|
|
215
|
+
await server.connect(new StdioServerTransport());
|
|
216
|
+
console.error(`Rhythmic MCP server ready (API: ${BASE})`);
|
|
217
|
+
}
|
|
218
|
+
main().catch((e) => {
|
|
219
|
+
console.error("Fatal:", e);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rhythmic-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Rhythmic — exposes your spatial project board to coding agents (Claude Code, Cursor, Claude Desktop) as typed tools.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rhythmic-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"rhythmic",
|
|
18
|
+
"claude",
|
|
19
|
+
"cursor",
|
|
20
|
+
"agents",
|
|
21
|
+
"project-management"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/stevegrehan/rhythmic/tree/main/packages/mcp#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/stevegrehan/rhythmic.git",
|
|
27
|
+
"directory": "packages/mcp"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/stevegrehan/rhythmic/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"dev": "tsx src/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
46
|
+
"zod": "^3.25.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.17.0",
|
|
50
|
+
"tsx": "^4.19.2",
|
|
51
|
+
"typescript": "^5.7.3"
|
|
52
|
+
}
|
|
53
|
+
}
|