@mnicole-dev/rize-mcp-server 1.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.
Files changed (3) hide show
  1. package/README.md +194 -0
  2. package/dist/index.js +857 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @mnicole-dev/rize-mcp-server
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the [Rize](https://rize.io) time tracking GraphQL API. Track focus sessions, monitor app usage, manage projects/tasks/clients, and analyze productivity from any MCP-compatible client.
4
+
5
+ ## Features
6
+
7
+ **34 tools** covering the full Rize GraphQL API:
8
+
9
+ ### User (1 tool)
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `get-me` | Get the authenticated user profile |
13
+
14
+ ### Summaries & Analytics (3 tools)
15
+ | Tool | Description |
16
+ |------|-------------|
17
+ | `get-summaries` | Get time tracking summaries (focus, break, meeting, tracked time) with daily/weekly/monthly breakdown and optional category analysis |
18
+ | `get-apps-and-websites` | Get app and website usage for a time range |
19
+ | `get-categories` | Get time spent by category |
20
+
21
+ ### Sessions (7 tools)
22
+ | Tool | Description |
23
+ |------|-------------|
24
+ | `list-sessions` | List focus/break/meeting sessions |
25
+ | `get-current-session` | Get the currently active session |
26
+ | `start-session-timer` | Start a focus, break, or meeting timer |
27
+ | `stop-session-timer` | Stop the current timer |
28
+ | `extend-current-session` | Extend the current timer by N minutes |
29
+ | `create-session` | Create a manual session entry |
30
+ | `update-session` | Update a session |
31
+ | `delete-session` | Delete a session |
32
+
33
+ ### Projects (5 tools)
34
+ | Tool | Description |
35
+ |------|-------------|
36
+ | `list-projects` | List all projects with time spent and budgets |
37
+ | `get-project` | Get project details |
38
+ | `create-project` | Create a project (with keywords, budget, client) |
39
+ | `update-project` | Update a project |
40
+ | `delete-project` | Delete a project |
41
+
42
+ ### Tasks (4 tools)
43
+ | Tool | Description |
44
+ |------|-------------|
45
+ | `list-tasks` | List all tasks |
46
+ | `create-task` | Create a task (with project, assignee, keywords) |
47
+ | `update-task` | Update a task |
48
+ | `delete-task` | Delete a task |
49
+
50
+ ### Clients (4 tools)
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ | `list-clients` | List all clients |
54
+ | `create-client` | Create a client (with hourly rate, keywords) |
55
+ | `update-client` | Update a client |
56
+ | `delete-client` | Delete a client |
57
+
58
+ ### Time Entries (9 tools)
59
+ | Tool | Description |
60
+ |------|-------------|
61
+ | `list-project-time-entries` | List time entries for projects |
62
+ | `create-project-time-entry` | Log time against a project |
63
+ | `delete-project-time-entry` | Delete a project time entry |
64
+ | `list-task-time-entries` | List time entries for tasks |
65
+ | `create-task-time-entry` | Log time against a task |
66
+ | `delete-task-time-entry` | Delete a task time entry |
67
+ | `list-client-time-entries` | List time entries for clients |
68
+ | `create-client-time-entry` | Log time against a client |
69
+ | `delete-client-time-entry` | Delete a client time entry |
70
+
71
+ ## Requirements
72
+
73
+ - Node.js 18+
74
+ - A Rize account with API access
75
+ - An API key from [Settings > API](https://app.rize.io) in your Rize account
76
+
77
+ ## Installation
78
+
79
+ ```bash
80
+ npm install -g @mnicole-dev/rize-mcp-server
81
+ ```
82
+
83
+ Or run directly with `npx`:
84
+
85
+ ```bash
86
+ npx @mnicole-dev/rize-mcp-server
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ Set the `RIZE_API_KEY` environment variable:
92
+
93
+ ```bash
94
+ export RIZE_API_KEY=your-api-key
95
+ ```
96
+
97
+ ### Claude Code
98
+
99
+ Add to your `~/.claude.json`:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "rize": {
105
+ "type": "stdio",
106
+ "command": "npx",
107
+ "args": ["-y", "@mnicole-dev/rize-mcp-server"],
108
+ "env": {
109
+ "RIZE_API_KEY": "your-api-key"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Claude Desktop
117
+
118
+ Add to your config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "rize": {
124
+ "command": "npx",
125
+ "args": ["-y", "@mnicole-dev/rize-mcp-server"],
126
+ "env": {
127
+ "RIZE_API_KEY": "your-api-key"
128
+ }
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Examples
135
+
136
+ ### Weekly summary
137
+
138
+ ```
139
+ > Show me my time tracking summary for this week
140
+ ```
141
+
142
+ Calls `get-summaries` — returns tracked time, focus time, break time, meeting time, daily breakdown, and category analysis.
143
+
144
+ ### Start a focus session
145
+
146
+ ```
147
+ > Start a 45 minute focus session for deep work
148
+ ```
149
+
150
+ Calls `start-session-timer` with:
151
+ ```json
152
+ {
153
+ "type": "focus",
154
+ "length": 45,
155
+ "intention": "deep work"
156
+ }
157
+ ```
158
+
159
+ ### Check what apps you used
160
+
161
+ ```
162
+ > What apps did I use today?
163
+ ```
164
+
165
+ Calls `get-apps-and-websites` with today's date range — shows time spent per app with category labels.
166
+
167
+ ### Log time to a project
168
+
169
+ ```
170
+ > Log 2 hours on project abc123 from 9am to 11am today
171
+ ```
172
+
173
+ Calls `create-project-time-entry` with the project ID and time range.
174
+
175
+ ## How it works
176
+
177
+ 1. The MCP client sends a tool call to the server via stdio
178
+ 2. The server translates the call into a GraphQL query/mutation
179
+ 3. The request is sent to `https://api.rize.io/api/v1/graphql` with Bearer token auth
180
+ 4. The response is formatted as human-readable text and returned
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ git clone https://github.com/mnicole-dev/rize-mcp-server.git
186
+ cd rize-mcp-server
187
+ pnpm install
188
+ pnpm dev # Run with tsx (requires RIZE_API_KEY)
189
+ pnpm build # Build to dist/
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ var API_URL = "https://api.rize.io/api/v1/graphql";
8
+ function getApiKey() {
9
+ const key = process.env["RIZE_API_KEY"];
10
+ if (!key) throw new Error("RIZE_API_KEY environment variable is required");
11
+ return key;
12
+ }
13
+ async function gql(query, variables) {
14
+ const resp = await fetch(API_URL, {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ Authorization: `Bearer ${getApiKey()}`
19
+ },
20
+ body: JSON.stringify({ query, variables })
21
+ });
22
+ const json = await resp.json();
23
+ if (json.errors?.length) {
24
+ throw new Error(json.errors.map((e) => e.message).join("; "));
25
+ }
26
+ return json.data;
27
+ }
28
+ function textResult(text) {
29
+ return { content: [{ type: "text", text }] };
30
+ }
31
+ function formatDuration(seconds) {
32
+ const h = Math.floor(seconds / 3600);
33
+ const m = Math.floor(seconds % 3600 / 60);
34
+ if (h === 0) return `${m}m`;
35
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
36
+ }
37
+ function today() {
38
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
39
+ }
40
+ var server = new McpServer({
41
+ name: "rize-mcp-server",
42
+ version: "1.0.0"
43
+ });
44
+ server.tool("get-me", "Get the currently authenticated Rize user.", {}, async () => {
45
+ const data = await gql("{ currentUser { email name } }");
46
+ const u = data.currentUser;
47
+ return textResult(`**${u.name}**
48
+ Email: ${u.email}`);
49
+ });
50
+ server.tool(
51
+ "get-summaries",
52
+ "Get time tracking summaries (focus, break, meeting, tracked time) for a date range.",
53
+ {
54
+ startDate: z.string().default("").describe("Start date (YYYY-MM-DD). Defaults to today."),
55
+ endDate: z.string().default("").describe("End date (YYYY-MM-DD). Defaults to today."),
56
+ bucketSize: z.enum(["day", "week", "month"]).default("day").describe("Bucket size for breakdown"),
57
+ includeCategories: z.boolean().default(false).describe("Include category breakdown")
58
+ },
59
+ async (params) => {
60
+ const start = params.startDate || today();
61
+ const end = params.endDate || today();
62
+ const data = await gql(
63
+ `query($start: ISO8601Date!, $end: ISO8601Date!, $bucket: String!, $cats: Boolean) {
64
+ summaries(startDate: $start, endDate: $end, bucketSize: $bucket, includeCategories: $cats) {
65
+ startTime endTime bucketSize
66
+ trackedTime trackedTimeAverage
67
+ focusTime focusTimeAverage
68
+ breakTime breakTimeAverage
69
+ meetingTime meetingTimeAverage
70
+ workHours workHoursAverage workHoursTarget
71
+ buckets { date wday trackedTime focusTime breakTime meetingTime workHours pto }
72
+ categories { category { name key work focus } timeSpent }
73
+ }
74
+ }`,
75
+ { start, end, bucket: params.bucketSize, cats: params.includeCategories }
76
+ );
77
+ const s = data.summaries;
78
+ const lines = [
79
+ `**Summary ${start} \u2192 ${end}**`,
80
+ `Tracked: ${formatDuration(s.trackedTime)} | Focus: ${formatDuration(s.focusTime)} | Break: ${formatDuration(s.breakTime)} | Meeting: ${formatDuration(s.meetingTime)}`,
81
+ `Work hours: ${formatDuration(s.workHours)} / ${formatDuration(s.workHoursTarget)} target`
82
+ ];
83
+ if (s.buckets.length > 0 && s.buckets.length <= 31) {
84
+ lines.push("", "**Daily breakdown:**");
85
+ for (const b of s.buckets) {
86
+ const pto = b.pto ? " [PTO]" : "";
87
+ lines.push(
88
+ `- ${b.date} (${b.wday}): tracked ${formatDuration(b.trackedTime)}, focus ${formatDuration(b.focusTime)}, work ${formatDuration(b.workHours)}${pto}`
89
+ );
90
+ }
91
+ }
92
+ if (s.categories?.length) {
93
+ lines.push("", "**Categories:**");
94
+ for (const c of s.categories.sort((a, b) => b.timeSpent - a.timeSpent).slice(0, 15)) {
95
+ lines.push(`- ${c.category.name}: ${formatDuration(c.timeSpent)}`);
96
+ }
97
+ }
98
+ return textResult(lines.join("\n"));
99
+ }
100
+ );
101
+ server.tool(
102
+ "get-apps-and-websites",
103
+ "Get apps and websites usage for a time range, showing where time was spent.",
104
+ {
105
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
106
+ endTime: z.string().describe("End datetime (ISO 8601)")
107
+ },
108
+ async ({ startTime, endTime }) => {
109
+ const data = await gql(
110
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime) {
111
+ appsAndWebsites(startTime: $start, endTime: $end) {
112
+ appName title url urlHost type timeSpent
113
+ timeCategory { name key work focus }
114
+ }
115
+ }`,
116
+ { start: startTime, end: endTime }
117
+ );
118
+ const apps = data.appsAndWebsites.sort((a, b) => b.timeSpent - a.timeSpent).slice(0, 30);
119
+ if (apps.length === 0) return textResult("No app/website activity found.");
120
+ const lines = apps.map((a) => {
121
+ const cat = a.timeCategory ? ` [${a.timeCategory.name}]` : "";
122
+ const host = a.urlHost ? ` (${a.urlHost})` : "";
123
+ return `- **${a.appName || a.title}**${host}: ${formatDuration(a.timeSpent)}${cat}`;
124
+ });
125
+ return textResult(lines.join("\n"));
126
+ }
127
+ );
128
+ server.tool(
129
+ "get-categories",
130
+ "Get time spent by category for a time range.",
131
+ {
132
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
133
+ endTime: z.string().describe("End datetime (ISO 8601)")
134
+ },
135
+ async ({ startTime, endTime }) => {
136
+ const data = await gql(
137
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime) {
138
+ categories(startTime: $start, endTime: $end) {
139
+ category { name key work focus idle } timeSpent
140
+ }
141
+ }`,
142
+ { start: startTime, end: endTime }
143
+ );
144
+ const cats = data.categories.sort((a, b) => b.timeSpent - a.timeSpent);
145
+ if (cats.length === 0) return textResult("No category data found.");
146
+ const lines = cats.map((c) => {
147
+ const tags = [
148
+ c.category.work ? "work" : "",
149
+ c.category.focus ? "focus" : "",
150
+ c.category.idle ? "idle" : ""
151
+ ].filter(Boolean).join(", ");
152
+ return `- **${c.category.name}**: ${formatDuration(c.timeSpent)}${tags ? ` (${tags})` : ""}`;
153
+ });
154
+ return textResult(lines.join("\n"));
155
+ }
156
+ );
157
+ server.tool(
158
+ "list-sessions",
159
+ "List focus/break/meeting sessions for a time range.",
160
+ {
161
+ startTime: z.string().optional().describe("Start datetime (ISO 8601)"),
162
+ endTime: z.string().optional().describe("End datetime (ISO 8601)"),
163
+ sort: z.enum(["created_at", "updated_at", "start_time"]).default("start_time").describe("Sort order")
164
+ },
165
+ async (params) => {
166
+ const data = await gql(
167
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime, $sort: TimeEntrySortEnum) {
168
+ sessions(startTime: $start, endTime: $end, sort: $sort) {
169
+ id title description type source startTime endTime
170
+ }
171
+ }`,
172
+ { start: params.startTime, end: params.endTime, sort: params.sort }
173
+ );
174
+ if (data.sessions.length === 0) return textResult("No sessions found.");
175
+ const lines = data.sessions.map((s) => {
176
+ const title = s.title ? ` \u2014 ${s.title}` : "";
177
+ const desc = s.description ? ` (${s.description})` : "";
178
+ const end = s.endTime ? s.endTime.slice(11, 16) : "running";
179
+ return `- **[${s.type}]** ${s.startTime.slice(0, 10)} ${s.startTime.slice(11, 16)}\u2192${end}${title}${desc} (id: ${s.id})`;
180
+ });
181
+ return textResult(lines.join("\n"));
182
+ }
183
+ );
184
+ server.tool(
185
+ "get-current-session",
186
+ "Get the currently active session (focus, break, or meeting).",
187
+ {},
188
+ async () => {
189
+ const data = await gql(
190
+ `{ currentSession { id title description type source startTime endTime } }`
191
+ );
192
+ if (!data.currentSession) return textResult("No active session.");
193
+ const s = data.currentSession;
194
+ const title = s.title ? `
195
+ Title: ${s.title}` : "";
196
+ const desc = s.description ? `
197
+ Description: ${s.description}` : "";
198
+ return textResult(
199
+ `**Active session: ${s.type}**
200
+ Started: ${s.startTime}${title}${desc}
201
+ ID: ${s.id}`
202
+ );
203
+ }
204
+ );
205
+ server.tool(
206
+ "start-session-timer",
207
+ "Start a new focus, break, or meeting session timer.",
208
+ {
209
+ type: z.enum(["focus", "break", "meeting", "custom", "other"]).describe("Session type"),
210
+ length: z.number().optional().describe("Duration in minutes"),
211
+ title: z.string().optional().describe("Session title"),
212
+ intention: z.string().optional().describe("What you intend to work on")
213
+ },
214
+ async (params) => {
215
+ const input = { type: params.type };
216
+ if (params.length) input["length"] = params.length;
217
+ if (params.title) input["title"] = params.title;
218
+ if (params.intention) input["intention"] = params.intention;
219
+ const data = await gql(
220
+ `mutation($input: StartSessionTimerInput!) {
221
+ startSessionTimer(input: $input) { session { id type title startTime } errors }
222
+ }`,
223
+ { input }
224
+ );
225
+ const s = data.startSessionTimer.session;
226
+ const title = s.title ? ` \u2014 ${s.title}` : "";
227
+ return textResult(`Session started!
228
+ - **${s.type}**${title}
229
+ - Started: ${s.startTime}
230
+ - ID: ${s.id}`);
231
+ }
232
+ );
233
+ server.tool(
234
+ "stop-session-timer",
235
+ "Stop the currently running session timer.",
236
+ {},
237
+ async () => {
238
+ const data = await gql(
239
+ `mutation($input: StopSessionTimerInput!) {
240
+ stopSessionTimer(input: $input) { session { id type title startTime endTime } errors }
241
+ }`,
242
+ { input: {} }
243
+ );
244
+ const s = data.stopSessionTimer.session;
245
+ return textResult(`Session stopped!
246
+ - **${s.type}** ${s.startTime.slice(11, 16)}\u2192${s.endTime.slice(11, 16)}
247
+ - ID: ${s.id}`);
248
+ }
249
+ );
250
+ server.tool(
251
+ "extend-current-session",
252
+ "Extend the current session timer by a number of minutes.",
253
+ { length: z.number().describe("Minutes to extend by") },
254
+ async ({ length }) => {
255
+ const data = await gql(
256
+ `mutation($input: ExtendCurrentSessionInput!) {
257
+ extendCurrentSession(input: $input) { session { id type endTime } errors }
258
+ }`,
259
+ { input: { length } }
260
+ );
261
+ const s = data.extendCurrentSession.session;
262
+ return textResult(`Session extended!
263
+ - **${s.type}** now ends at ${s.endTime.slice(11, 16)}
264
+ - ID: ${s.id}`);
265
+ }
266
+ );
267
+ server.tool(
268
+ "create-session",
269
+ "Create a manual session entry.",
270
+ {
271
+ type: z.enum(["focus", "break", "meeting", "custom", "other"]).describe("Session type"),
272
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
273
+ endTime: z.string().describe("End datetime (ISO 8601)"),
274
+ title: z.string().optional().describe("Session title"),
275
+ description: z.string().optional().describe("Session description")
276
+ },
277
+ async (params) => {
278
+ const args = {
279
+ type: params.type,
280
+ startTime: params.startTime,
281
+ endTime: params.endTime
282
+ };
283
+ if (params.title) args["title"] = params.title;
284
+ if (params.description) args["description"] = params.description;
285
+ const data = await gql(
286
+ `mutation($input: CreateSessionInput!) {
287
+ createSession(input: $input) { session { id type title startTime endTime } errors }
288
+ }`,
289
+ { input: { args } }
290
+ );
291
+ const s = data.createSession.session;
292
+ return textResult(`Session created!
293
+ - **${s.type}** ${s.startTime.slice(11, 16)}\u2192${s.endTime.slice(11, 16)}
294
+ - ID: ${s.id}`);
295
+ }
296
+ );
297
+ server.tool(
298
+ "update-session",
299
+ "Update an existing session.",
300
+ {
301
+ id: z.string().describe("Session ID"),
302
+ title: z.string().optional().describe("New title"),
303
+ description: z.string().optional().describe("New description"),
304
+ type: z.enum(["focus", "break", "meeting", "custom", "other"]).optional().describe("New type"),
305
+ startTime: z.string().optional().describe("New start time (ISO 8601)"),
306
+ endTime: z.string().optional().describe("New end time (ISO 8601)")
307
+ },
308
+ async (params) => {
309
+ const args = { id: params.id };
310
+ if (params.title !== void 0) args["title"] = params.title;
311
+ if (params.description !== void 0) args["description"] = params.description;
312
+ if (params.type) args["type"] = params.type;
313
+ if (params.startTime) args["startTime"] = params.startTime;
314
+ if (params.endTime) args["endTime"] = params.endTime;
315
+ const data = await gql(
316
+ `mutation($input: UpdateSessionInput!) {
317
+ updateSession(input: $input) { session { id type title } errors }
318
+ }`,
319
+ { input: { args } }
320
+ );
321
+ const s = data.updateSession.session;
322
+ return textResult(`Session updated: **${s.type}**${s.title ? ` \u2014 ${s.title}` : ""} (id: ${s.id})`);
323
+ }
324
+ );
325
+ server.tool(
326
+ "delete-session",
327
+ "Delete a session by ID.",
328
+ { id: z.string().describe("Session ID") },
329
+ async ({ id }) => {
330
+ await gql(
331
+ `mutation($input: DeleteSessionInput!) { deleteSession(input: $input) { session { id } errors } }`,
332
+ { input: { id } }
333
+ );
334
+ return textResult(`Session ${id} deleted.`);
335
+ }
336
+ );
337
+ server.tool(
338
+ "list-projects",
339
+ "List all projects.",
340
+ {
341
+ query: z.string().optional().describe("Search by name"),
342
+ sort: z.enum(["created_at", "updated_at", "last_used_at"]).optional().describe("Sort order")
343
+ },
344
+ async (params) => {
345
+ const vars = {};
346
+ if (params.query) vars["query"] = params.query;
347
+ if (params.sort) vars["sort"] = params.sort;
348
+ const data = await gql(
349
+ `query($query: String, $sort: SortEnum) {
350
+ projects(query: $query, sort: $sort, first: 100) {
351
+ nodes { id name status color emoji totalTimeSpent timeBudget timeBudgetInterval client { name } }
352
+ }
353
+ }`,
354
+ vars
355
+ );
356
+ const projects = data.projects.nodes;
357
+ if (projects.length === 0) return textResult("No projects found.");
358
+ const lines = projects.map((p) => {
359
+ const client = p.client ? ` \u2014 ${p.client.name}` : "";
360
+ const budget = p.timeBudget ? ` / ${formatDuration(p.timeBudget)} budget` : "";
361
+ const emoji = p.emoji ? `${p.emoji} ` : "";
362
+ return `- ${emoji}**${p.name}**${client}: ${formatDuration(p.totalTimeSpent)}${budget} [${p.status}] (id: ${p.id})`;
363
+ });
364
+ return textResult(lines.join("\n"));
365
+ }
366
+ );
367
+ server.tool(
368
+ "get-project",
369
+ "Get a project by ID.",
370
+ { id: z.string().describe("Project ID") },
371
+ async ({ id }) => {
372
+ const data = await gql(
373
+ `query($id: ID!) { project(id: $id) { id name status color emoji totalTimeSpent timeBudget timeBudgetInterval keywords client { name } } }`,
374
+ { id }
375
+ );
376
+ const p = data.project;
377
+ const client = p.client ? `
378
+ Client: ${p.client.name}` : "";
379
+ const budget = p.timeBudget ? `
380
+ Budget: ${formatDuration(p.timeBudget)} per ${p.timeBudgetInterval}` : "";
381
+ const kw = p.keywords?.length ? `
382
+ Keywords: ${p.keywords.join(", ")}` : "";
383
+ return textResult(`**${p.name}**
384
+ Status: ${p.status}
385
+ Total time: ${formatDuration(p.totalTimeSpent)}${budget}${client}${kw}
386
+ ID: ${p.id}`);
387
+ }
388
+ );
389
+ server.tool(
390
+ "create-project",
391
+ "Create a new project.",
392
+ {
393
+ name: z.string().describe("Project name"),
394
+ color: z.string().optional().describe("Color hex code"),
395
+ emoji: z.string().optional().describe("Emoji"),
396
+ keywords: z.array(z.string()).optional().describe("Keywords for auto-categorization"),
397
+ timeBudget: z.number().optional().describe("Time budget in seconds"),
398
+ timeBudgetInterval: z.string().optional().describe('Budget interval (e.g. "weekly")'),
399
+ clientName: z.string().optional().describe("Client name to associate")
400
+ },
401
+ async (params) => {
402
+ const args = { name: params.name };
403
+ if (params.color) args["color"] = params.color;
404
+ if (params.emoji) args["emoji"] = params.emoji;
405
+ if (params.keywords) args["keywords"] = params.keywords;
406
+ if (params.timeBudget) args["timeBudget"] = params.timeBudget;
407
+ if (params.timeBudgetInterval) args["timeBudgetInterval"] = params.timeBudgetInterval;
408
+ if (params.clientName) args["clientName"] = params.clientName;
409
+ const data = await gql(
410
+ `mutation($input: CreateProjectInput!) { createProject(input: $input) { project { id name } errors } }`,
411
+ { input: { args } }
412
+ );
413
+ return textResult(`Project created: **${data.createProject.project.name}** (id: ${data.createProject.project.id})`);
414
+ }
415
+ );
416
+ server.tool(
417
+ "update-project",
418
+ "Update an existing project.",
419
+ {
420
+ id: z.string().describe("Project ID"),
421
+ name: z.string().optional().describe("New name"),
422
+ status: z.string().optional().describe("New status"),
423
+ color: z.string().optional().describe("Color hex code"),
424
+ emoji: z.string().optional().describe("Emoji"),
425
+ keywords: z.array(z.string()).optional().describe("Keywords"),
426
+ timeBudget: z.number().optional().describe("Time budget in seconds"),
427
+ timeBudgetInterval: z.string().optional().describe("Budget interval")
428
+ },
429
+ async (params) => {
430
+ const args = { id: params.id };
431
+ if (params.name) args["name"] = params.name;
432
+ if (params.status) args["status"] = params.status;
433
+ if (params.color) args["color"] = params.color;
434
+ if (params.emoji) args["emoji"] = params.emoji;
435
+ if (params.keywords) args["keywords"] = params.keywords;
436
+ if (params.timeBudget !== void 0) args["timeBudget"] = params.timeBudget;
437
+ if (params.timeBudgetInterval) args["timeBudgetInterval"] = params.timeBudgetInterval;
438
+ const data = await gql(
439
+ `mutation($input: UpdateProjectInput!) { updateProject(input: $input) { project { id name } errors } }`,
440
+ { input: { args } }
441
+ );
442
+ return textResult(`Project updated: **${data.updateProject.project.name}** (id: ${data.updateProject.project.id})`);
443
+ }
444
+ );
445
+ server.tool(
446
+ "delete-project",
447
+ "Delete a project by ID.",
448
+ { id: z.string().describe("Project ID") },
449
+ async ({ id }) => {
450
+ await gql(
451
+ `mutation($input: DeleteProjectInput!) { deleteProject(input: $input) { project { id } errors } }`,
452
+ { input: { id } }
453
+ );
454
+ return textResult(`Project ${id} deleted.`);
455
+ }
456
+ );
457
+ server.tool(
458
+ "list-tasks",
459
+ "List all tasks.",
460
+ {
461
+ query: z.string().optional().describe("Search by name"),
462
+ sort: z.enum(["created_at", "updated_at", "last_used_at"]).optional().describe("Sort order")
463
+ },
464
+ async (params) => {
465
+ const vars = {};
466
+ if (params.query) vars["query"] = params.query;
467
+ if (params.sort) vars["sort"] = params.sort;
468
+ const data = await gql(
469
+ `query($query: String, $sort: SortEnum) {
470
+ tasks(query: $query, sort: $sort, first: 100) {
471
+ nodes { id name status emoji totalTimeSpent project { name } assignee { name } }
472
+ }
473
+ }`,
474
+ vars
475
+ );
476
+ const tasks = data.tasks.nodes;
477
+ if (tasks.length === 0) return textResult("No tasks found.");
478
+ const lines = tasks.map((t) => {
479
+ const proj = t.project ? ` (${t.project.name})` : "";
480
+ const emoji = t.emoji ? `${t.emoji} ` : "";
481
+ return `- ${emoji}**${t.name}**${proj}: ${formatDuration(t.totalTimeSpent)} [${t.status}] (id: ${t.id})`;
482
+ });
483
+ return textResult(lines.join("\n"));
484
+ }
485
+ );
486
+ server.tool(
487
+ "create-task",
488
+ "Create a new task.",
489
+ {
490
+ name: z.string().describe("Task name"),
491
+ projectName: z.string().optional().describe("Project name to associate"),
492
+ assigneeEmail: z.string().optional().describe("Assignee email"),
493
+ color: z.string().optional().describe("Color hex code"),
494
+ emoji: z.string().optional().describe("Emoji"),
495
+ keywords: z.array(z.string()).optional().describe("Keywords"),
496
+ timeBudget: z.number().optional().describe("Time budget in seconds"),
497
+ timeBudgetInterval: z.string().optional().describe("Budget interval")
498
+ },
499
+ async (params) => {
500
+ const args = { name: params.name };
501
+ if (params.projectName) args["projectName"] = params.projectName;
502
+ if (params.assigneeEmail) args["assigneeEmail"] = params.assigneeEmail;
503
+ if (params.color) args["color"] = params.color;
504
+ if (params.emoji) args["emoji"] = params.emoji;
505
+ if (params.keywords) args["keywords"] = params.keywords;
506
+ if (params.timeBudget) args["timeBudget"] = params.timeBudget;
507
+ if (params.timeBudgetInterval) args["timeBudgetInterval"] = params.timeBudgetInterval;
508
+ const data = await gql(
509
+ `mutation($input: CreateTaskInput!) { createTask(input: $input) { task { id name } errors } }`,
510
+ { input: { args } }
511
+ );
512
+ return textResult(`Task created: **${data.createTask.task.name}** (id: ${data.createTask.task.id})`);
513
+ }
514
+ );
515
+ server.tool(
516
+ "update-task",
517
+ "Update an existing task.",
518
+ {
519
+ id: z.string().describe("Task ID"),
520
+ name: z.string().optional().describe("New name"),
521
+ status: z.string().optional().describe("New status"),
522
+ color: z.string().optional().describe("Color hex code"),
523
+ emoji: z.string().optional().describe("Emoji"),
524
+ keywords: z.array(z.string()).optional().describe("Keywords"),
525
+ projectName: z.string().optional().describe("Project name"),
526
+ assigneeEmail: z.string().optional().describe("Assignee email")
527
+ },
528
+ async (params) => {
529
+ const args = { id: params.id };
530
+ if (params.name) args["name"] = params.name;
531
+ if (params.status) args["status"] = params.status;
532
+ if (params.color) args["color"] = params.color;
533
+ if (params.emoji) args["emoji"] = params.emoji;
534
+ if (params.keywords) args["keywords"] = params.keywords;
535
+ if (params.projectName) args["projectName"] = params.projectName;
536
+ if (params.assigneeEmail) args["assigneeEmail"] = params.assigneeEmail;
537
+ const data = await gql(
538
+ `mutation($input: UpdateTaskInput!) { updateTask(input: $input) { task { id name } errors } }`,
539
+ { input: { args } }
540
+ );
541
+ return textResult(`Task updated: **${data.updateTask.task.name}** (id: ${data.updateTask.task.id})`);
542
+ }
543
+ );
544
+ server.tool(
545
+ "delete-task",
546
+ "Delete a task by ID.",
547
+ { id: z.string().describe("Task ID") },
548
+ async ({ id }) => {
549
+ await gql(
550
+ `mutation($input: DeleteTaskInput!) { deleteTask(input: $input) { task { id } errors } }`,
551
+ { input: { id } }
552
+ );
553
+ return textResult(`Task ${id} deleted.`);
554
+ }
555
+ );
556
+ server.tool(
557
+ "list-clients",
558
+ "List all clients.",
559
+ {
560
+ query: z.string().optional().describe("Search by name"),
561
+ sort: z.enum(["created_at", "updated_at", "last_used_at"]).optional().describe("Sort order")
562
+ },
563
+ async (params) => {
564
+ const vars = {};
565
+ if (params.query) vars["query"] = params.query;
566
+ if (params.sort) vars["sort"] = params.sort;
567
+ const data = await gql(
568
+ `query($query: String, $sort: SortEnum) {
569
+ clients(query: $query, sort: $sort, first: 100) {
570
+ nodes { id name status emoji hourlyRate totalTimeSpent }
571
+ }
572
+ }`,
573
+ vars
574
+ );
575
+ const clients = data.clients.nodes;
576
+ if (clients.length === 0) return textResult("No clients found.");
577
+ const lines = clients.map((c) => {
578
+ const rate = c.hourlyRate ? ` @ ${c.hourlyRate}/h` : "";
579
+ const emoji = c.emoji ? `${c.emoji} ` : "";
580
+ return `- ${emoji}**${c.name}**: ${formatDuration(c.totalTimeSpent)}${rate} [${c.status}] (id: ${c.id})`;
581
+ });
582
+ return textResult(lines.join("\n"));
583
+ }
584
+ );
585
+ server.tool(
586
+ "create-client",
587
+ "Create a new client.",
588
+ {
589
+ name: z.string().describe("Client name"),
590
+ color: z.string().optional().describe("Color hex code"),
591
+ emoji: z.string().optional().describe("Emoji"),
592
+ hourlyRate: z.number().optional().describe("Hourly rate"),
593
+ keywords: z.array(z.string()).optional().describe("Keywords"),
594
+ timeBudget: z.number().optional().describe("Time budget in seconds"),
595
+ timeBudgetInterval: z.string().optional().describe("Budget interval")
596
+ },
597
+ async (params) => {
598
+ const args = { name: params.name };
599
+ if (params.color) args["color"] = params.color;
600
+ if (params.emoji) args["emoji"] = params.emoji;
601
+ if (params.hourlyRate !== void 0) args["hourlyRate"] = params.hourlyRate;
602
+ if (params.keywords) args["keywords"] = params.keywords;
603
+ if (params.timeBudget) args["timeBudget"] = params.timeBudget;
604
+ if (params.timeBudgetInterval) args["timeBudgetInterval"] = params.timeBudgetInterval;
605
+ const data = await gql(
606
+ `mutation($input: CreateClientInput!) { createClient(input: $input) { client { id name } errors } }`,
607
+ { input: { args } }
608
+ );
609
+ return textResult(`Client created: **${data.createClient.client.name}** (id: ${data.createClient.client.id})`);
610
+ }
611
+ );
612
+ server.tool(
613
+ "update-client",
614
+ "Update an existing client.",
615
+ {
616
+ id: z.string().describe("Client ID"),
617
+ name: z.string().optional().describe("New name"),
618
+ status: z.string().optional().describe("New status"),
619
+ color: z.string().optional().describe("Color hex code"),
620
+ emoji: z.string().optional().describe("Emoji"),
621
+ hourlyRate: z.number().optional().describe("Hourly rate"),
622
+ keywords: z.array(z.string()).optional().describe("Keywords")
623
+ },
624
+ async (params) => {
625
+ const args = { id: params.id };
626
+ if (params.name) args["name"] = params.name;
627
+ if (params.status) args["status"] = params.status;
628
+ if (params.color) args["color"] = params.color;
629
+ if (params.emoji) args["emoji"] = params.emoji;
630
+ if (params.hourlyRate !== void 0) args["hourlyRate"] = params.hourlyRate;
631
+ if (params.keywords) args["keywords"] = params.keywords;
632
+ const data = await gql(
633
+ `mutation($input: UpdateClientInput!) { updateClient(input: $input) { client { id name } errors } }`,
634
+ { input: { args } }
635
+ );
636
+ return textResult(`Client updated: **${data.updateClient.client.name}** (id: ${data.updateClient.client.id})`);
637
+ }
638
+ );
639
+ server.tool(
640
+ "delete-client",
641
+ "Delete a client by ID.",
642
+ { id: z.string().describe("Client ID") },
643
+ async ({ id }) => {
644
+ await gql(
645
+ `mutation($input: DeleteClientInput!) { deleteClient(input: $input) { client { id } errors } }`,
646
+ { input: { id } }
647
+ );
648
+ return textResult(`Client ${id} deleted.`);
649
+ }
650
+ );
651
+ server.tool(
652
+ "list-project-time-entries",
653
+ "List time entries for projects.",
654
+ {
655
+ startTime: z.string().optional().describe("Start datetime (ISO 8601)"),
656
+ endTime: z.string().optional().describe("End datetime (ISO 8601)"),
657
+ sort: z.enum(["created_at", "updated_at", "start_time"]).default("start_time").describe("Sort")
658
+ },
659
+ async (params) => {
660
+ const data = await gql(
661
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime, $sort: TimeEntrySortEnum) {
662
+ projectTimeEntries(startTime: $start, endTime: $end, sort: $sort) {
663
+ id description duration startTime endTime source project { name }
664
+ }
665
+ }`,
666
+ { start: params.startTime, end: params.endTime, sort: params.sort }
667
+ );
668
+ const entries = data.projectTimeEntries;
669
+ if (entries.length === 0) return textResult("No project time entries found.");
670
+ const lines = entries.map((e) => {
671
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
672
+ return `- **${e.project.name}** ${e.startTime.slice(0, 10)}: ${formatDuration(e.duration)}${desc} (id: ${e.id})`;
673
+ });
674
+ return textResult(lines.join("\n"));
675
+ }
676
+ );
677
+ server.tool(
678
+ "create-project-time-entry",
679
+ "Create a time entry for a project.",
680
+ {
681
+ projectId: z.string().describe("Project ID"),
682
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
683
+ endTime: z.string().describe("End datetime (ISO 8601)"),
684
+ description: z.string().optional().describe("Description"),
685
+ billable: z.boolean().optional().describe("Billable")
686
+ },
687
+ async (params) => {
688
+ const input = {
689
+ projectId: params.projectId,
690
+ startTime: params.startTime,
691
+ endTime: params.endTime
692
+ };
693
+ if (params.description) input["description"] = params.description;
694
+ if (params.billable !== void 0) input["billable"] = params.billable;
695
+ const data = await gql(
696
+ `mutation($input: CreateProjectTimeEntryInput!) {
697
+ createProjectTimeEntry(input: $input) { timeEntry { id duration project { name } } errors }
698
+ }`,
699
+ { input }
700
+ );
701
+ const e = data.createProjectTimeEntry.timeEntry;
702
+ return textResult(`Time entry created: **${e.project.name}** ${formatDuration(e.duration)} (id: ${e.id})`);
703
+ }
704
+ );
705
+ server.tool(
706
+ "delete-project-time-entry",
707
+ "Delete a project time entry.",
708
+ { id: z.string().describe("Time entry ID") },
709
+ async ({ id }) => {
710
+ await gql(
711
+ `mutation($input: DeleteProjectTimeEntryInput!) { deleteProjectTimeEntry(input: $input) { timeEntry { id } errors } }`,
712
+ { input: { id } }
713
+ );
714
+ return textResult(`Project time entry ${id} deleted.`);
715
+ }
716
+ );
717
+ server.tool(
718
+ "list-task-time-entries",
719
+ "List time entries for tasks.",
720
+ {
721
+ startTime: z.string().optional().describe("Start datetime (ISO 8601)"),
722
+ endTime: z.string().optional().describe("End datetime (ISO 8601)"),
723
+ sort: z.enum(["created_at", "updated_at", "start_time"]).default("start_time").describe("Sort")
724
+ },
725
+ async (params) => {
726
+ const data = await gql(
727
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime, $sort: TimeEntrySortEnum) {
728
+ taskTimeEntries(startTime: $start, endTime: $end, sort: $sort) {
729
+ id description duration startTime endTime task { name }
730
+ }
731
+ }`,
732
+ { start: params.startTime, end: params.endTime, sort: params.sort }
733
+ );
734
+ const entries = data.taskTimeEntries;
735
+ if (entries.length === 0) return textResult("No task time entries found.");
736
+ const lines = entries.map((e) => {
737
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
738
+ return `- **${e.task.name}** ${e.startTime.slice(0, 10)}: ${formatDuration(e.duration)}${desc} (id: ${e.id})`;
739
+ });
740
+ return textResult(lines.join("\n"));
741
+ }
742
+ );
743
+ server.tool(
744
+ "create-task-time-entry",
745
+ "Create a time entry for a task.",
746
+ {
747
+ taskId: z.string().describe("Task ID"),
748
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
749
+ endTime: z.string().describe("End datetime (ISO 8601)"),
750
+ description: z.string().optional().describe("Description"),
751
+ billable: z.boolean().optional().describe("Billable")
752
+ },
753
+ async (params) => {
754
+ const input = {
755
+ taskId: params.taskId,
756
+ startTime: params.startTime,
757
+ endTime: params.endTime
758
+ };
759
+ if (params.description) input["description"] = params.description;
760
+ if (params.billable !== void 0) input["billable"] = params.billable;
761
+ const data = await gql(
762
+ `mutation($input: CreateTaskTimeEntryInput!) {
763
+ createTaskTimeEntry(input: $input) { timeEntry { id duration task { name } } errors }
764
+ }`,
765
+ { input }
766
+ );
767
+ const e = data.createTaskTimeEntry.timeEntry;
768
+ return textResult(`Time entry created: **${e.task.name}** ${formatDuration(e.duration)} (id: ${e.id})`);
769
+ }
770
+ );
771
+ server.tool(
772
+ "delete-task-time-entry",
773
+ "Delete a task time entry.",
774
+ { id: z.string().describe("Time entry ID") },
775
+ async ({ id }) => {
776
+ await gql(
777
+ `mutation($input: DeleteTaskTimeEntryInput!) { deleteTaskTimeEntry(input: $input) { timeEntry { id } errors } }`,
778
+ { input: { id } }
779
+ );
780
+ return textResult(`Task time entry ${id} deleted.`);
781
+ }
782
+ );
783
+ server.tool(
784
+ "list-client-time-entries",
785
+ "List time entries for clients.",
786
+ {
787
+ startTime: z.string().optional().describe("Start datetime (ISO 8601)"),
788
+ endTime: z.string().optional().describe("End datetime (ISO 8601)"),
789
+ sort: z.enum(["created_at", "updated_at", "start_time"]).default("start_time").describe("Sort")
790
+ },
791
+ async (params) => {
792
+ const data = await gql(
793
+ `query($start: ISO8601DateTime, $end: ISO8601DateTime, $sort: TimeEntrySortEnum) {
794
+ clientTimeEntries(startTime: $start, endTime: $end, sort: $sort) {
795
+ id description duration startTime endTime client { name }
796
+ }
797
+ }`,
798
+ { start: params.startTime, end: params.endTime, sort: params.sort }
799
+ );
800
+ const entries = data.clientTimeEntries;
801
+ if (entries.length === 0) return textResult("No client time entries found.");
802
+ const lines = entries.map((e) => {
803
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
804
+ return `- **${e.client.name}** ${e.startTime.slice(0, 10)}: ${formatDuration(e.duration)}${desc} (id: ${e.id})`;
805
+ });
806
+ return textResult(lines.join("\n"));
807
+ }
808
+ );
809
+ server.tool(
810
+ "create-client-time-entry",
811
+ "Create a time entry for a client.",
812
+ {
813
+ clientId: z.string().describe("Client ID"),
814
+ startTime: z.string().describe("Start datetime (ISO 8601)"),
815
+ endTime: z.string().describe("End datetime (ISO 8601)"),
816
+ description: z.string().optional().describe("Description"),
817
+ billable: z.boolean().optional().describe("Billable")
818
+ },
819
+ async (params) => {
820
+ const input = {
821
+ clientId: params.clientId,
822
+ startTime: params.startTime,
823
+ endTime: params.endTime
824
+ };
825
+ if (params.description) input["description"] = params.description;
826
+ if (params.billable !== void 0) input["billable"] = params.billable;
827
+ const data = await gql(
828
+ `mutation($input: CreateClientTimeEntryInput!) {
829
+ createClientTimeEntry(input: $input) { timeEntry { id duration client { name } } errors }
830
+ }`,
831
+ { input }
832
+ );
833
+ const e = data.createClientTimeEntry.timeEntry;
834
+ return textResult(`Time entry created: **${e.client.name}** ${formatDuration(e.duration)} (id: ${e.id})`);
835
+ }
836
+ );
837
+ server.tool(
838
+ "delete-client-time-entry",
839
+ "Delete a client time entry.",
840
+ { id: z.string().describe("Time entry ID") },
841
+ async ({ id }) => {
842
+ await gql(
843
+ `mutation($input: DeleteClientTimeEntryInput!) { deleteClientTimeEntry(input: $input) { timeEntry { id } errors } }`,
844
+ { input: { id } }
845
+ );
846
+ return textResult(`Client time entry ${id} deleted.`);
847
+ }
848
+ );
849
+ async function main() {
850
+ const transport = new StdioServerTransport();
851
+ await server.connect(transport);
852
+ }
853
+ main().catch((err) => {
854
+ process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
855
+ `);
856
+ process.exit(1);
857
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mnicole-dev/rize-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for the Rize time tracking GraphQL API",
5
+ "type": "module",
6
+ "bin": {
7
+ "rize-mcp-server": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --out-dir dist --clean",
15
+ "dev": "tsx src/index.ts"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "rize",
20
+ "time-tracking",
21
+ "focus",
22
+ "productivity",
23
+ "model-context-protocol"
24
+ ],
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.1",
28
+ "zod": "^4.3.6"
29
+ },
30
+ "devDependencies": {
31
+ "tsup": "^8.4.0",
32
+ "tsx": "^4.19.0",
33
+ "typescript": "^5.8.0"
34
+ }
35
+ }