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.
Files changed (3) hide show
  1. package/README.md +94 -0
  2. package/dist/index.js +221 -0
  3. 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
+ }