@timewave-cloud/mcp
Advanced tools
+1
-1
| { | ||
| "name": "@timewave-cloud/mcp", | ||
| "version": "0.3.1", | ||
| "version": "0.4.0", | ||
| "description": "TimeWave MCP server — manage TimeWave tasks and the time tracker from an AI client (Claude Desktop, IDEs) over the Model Context Protocol.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+34
-0
@@ -109,3 +109,37 @@ # TimeWave MCP server | ||
| | `delete_time_entry` | Permanently delete a time entry | | ||
| | `whoami` | Who the key belongs to, and which organization | | ||
| | `add_checklist_item` | Add a subtask to a task | | ||
| | `set_checklist_item` | Tick a subtask off (or back on) — `itemId` from `get_task` | | ||
| | `remove_checklist_item` | Remove a subtask | | ||
| | `list_timesheets` | Recent weeks: total, status, locked or not | | ||
| | `get_timesheet` | One week, day by day and project by project | | ||
| | `submit_timesheet` | Hand a week in — **this locks it** | | ||
| | `withdraw_timesheet` | Take a submitted week back (only before review) | | ||
| | `list_docs` | Search the knowledge base by title | | ||
| | `get_doc` | Read a document with its Markdown | | ||
| | `create_doc` | Write a new document | | ||
| | `update_doc` | Edit the title, body or visibility | | ||
| | `list_doc_versions` | A document's history — who changed what, when | | ||
| | `get_doc_version` | Read one earlier version in full | | ||
| ## The knowledge base has its own locks | ||
| Two of them, and a key can hold the `docs` scope and still see nothing: | ||
| - **The organization has to open the base to assistants** — Settings → | ||
| Assistants. Off until somebody turns it on. | ||
| - **A single page can be held back**, along with everything filed under it. To | ||
| an assistant that page reads exactly as one that does not exist; that is the | ||
| point, and a `not found` is not something to route around. | ||
| New documents are **private** — visible only to their author. Pass | ||
| `visibility: "ORG"` when writing for the team. | ||
| `update_doc` replaces the whole body. The previous text is kept as a version and | ||
| can be read back with `list_doc_versions` / `get_doc_version`, but somebody has | ||
| to go and look for it — so change the part that is wrong rather than rewriting a | ||
| page from memory. | ||
| There is no tool for deleting a document, on purpose. | ||
| ## Smart commits | ||
@@ -112,0 +146,0 @@ |
+258
-8
@@ -59,6 +59,7 @@ #!/usr/bin/env node | ||
| // Surfaced to the client at connect time — orients an AI to how TimeWave works. | ||
| const INSTRUCTIONS = `TimeWave is a time-tracker + task manager (projects → kanban tasks → tracked time & money). | ||
| When asked to work in a TimeWave project: call \`guide\` first, then \`list_projects\` and \`list_tasks\` to see the board. Manage work with create_task / update_task / move_task / complete_task / comment_task, and record effort with start_timer/stop_timer or log_time. | ||
| const INSTRUCTIONS = `TimeWave is a time-tracker + task manager with a knowledge base (projects → kanban tasks → tracked time & money, plus Markdown docs). | ||
| When asked to work in a TimeWave project: call \`guide\` first, then \`list_projects\` and \`list_tasks\` to see the board. Manage work with create_task / update_task / move_task / complete_task / comment_task, tick subtasks off with set_checklist_item, and record effort with start_timer/stop_timer or log_time. | ||
| The knowledge base is \`list_docs\` / \`get_doc\` / \`create_doc\` / \`update_doc\`, and it is closed to assistants until an owner opens it. | ||
| If you also write code for the project, use SMART COMMITS: put the task key in the commit message. "closes WAVE-12" completes that task and moves it to the last column; "WAVE-12 #time 2h" logs time. A bare "WAVE-12" just links the commit. Call \`guide\` for the full rules. | ||
| Every action you take is attributed to the AI (an "AI" badge in the app).`; | ||
| Every action you take is attributed to the AI (an "AI" badge in the app).` | ||
@@ -68,5 +69,8 @@ // Full reference returned by the `guide` tool. | ||
| TimeWave is a multi-tenant time tracker with a task manager. Structure: | ||
| **Organization → Projects → Tasks (on a kanban board) → tracked time (→ money)**. | ||
| TimeWave is a multi-tenant time tracker with a task manager and a knowledge base. | ||
| **Organization → Projects → Tasks (on a kanban board) → tracked time (→ money)**, | ||
| plus Markdown documents that hang off the organization or a project. | ||
| Call \`whoami\` if you need to know whose name will be on what you write. | ||
| ## Typical workflow | ||
@@ -79,2 +83,4 @@ 1. \`list_projects\` — projects and their kanban column names. Pick the project id. | ||
| - \`update_task\`, \`move_task\` (to a column by name/id), \`complete_task\`, \`comment_task\`, \`delete_task\`. | ||
| - Subtasks: \`add_checklist_item\`, \`set_checklist_item\` (tick one off — get \`itemId\` from \`get_task\`), \`remove_checklist_item\`. | ||
| If you wrote a checklist when you created the task, tick items as you finish them. A board showing 0/6 on work that is done is worse than no checklist. | ||
| 4. Record effort: | ||
@@ -85,2 +91,44 @@ - \`start_timer\` / \`stop_timer\` / \`timer_status\` for live tracking, or | ||
| ## Timesheets | ||
| A week of one person's time, handed in for approval. \`list_timesheets\` shows the | ||
| last few weeks with their status and whether they are locked; \`get_timesheet\` | ||
| breaks one week down by day and project. | ||
| \`submit_timesheet\` **locks** the week — no time can be added or edited in it | ||
| afterwards. It is somebody's statement about their own work, so ask before | ||
| submitting one. \`withdraw_timesheet\` takes it back, but only while nobody has | ||
| reviewed it; an approved week needs an admin to reopen. | ||
| A locked week is also why adding time can be refused. If \`log_time\` says the | ||
| week is locked, that is the rule working, not an error to route around. | ||
| ## Knowledge base | ||
| Markdown pages: \`list_docs\` (search titles), \`get_doc\` (one page with its text), | ||
| \`create_doc\`, \`update_doc\`, and the history — \`list_doc_versions\`, | ||
| \`get_doc_version\`. | ||
| Things worth knowing before you write: | ||
| - **It is closed to assistants until an owner opens it** in Settings → Knowledge | ||
| base, and this key also needs the \`docs\` scope. If you get told the base is | ||
| closed, say so and stop — that is a decision somebody made, not an obstacle. | ||
| - **Individual pages can be held back.** One that is reads exactly like a page | ||
| that does not exist. Do not treat a "not found" as something to work around. | ||
| - **New pages are PRIVATE** — visible only to their author. If you are writing | ||
| something for the team, pass \`visibility: "ORG"\`, or you have written a note | ||
| to one person and told them it is documentation. | ||
| - **\`update_doc\` replaces the whole body.** The previous text is kept as a | ||
| version, so nothing is lost — but a person has to go and find it, which they | ||
| will not think to do. Read the page first and change the part that is wrong, | ||
| rather than rewriting it from memory. | ||
| - **Do not put credentials in a document.** Passwords, tokens, private keys: a | ||
| knowledge base is built to be searched, shared and exported, which is exactly | ||
| what you do not want for a secret. Say where the secret lives instead. | ||
| - There is no delete tool, on purpose. Deleting documentation is not something | ||
| an assistant should be able to do in passing; ask a person to do it. | ||
| Attachments come back as metadata only. To fetch one, GET | ||
| \`/api/attachments/{id}\` with the same API key — not through this server, which | ||
| would put the whole file in the conversation. | ||
| ## Smart commits (when you commit code to a connected repo) | ||
@@ -94,3 +142,3 @@ Reference the task key (e.g. \`WAVE-12\`) in the commit message. On import (manual or the push webhook) TimeWave acts on it: | ||
| Rules & caveats: | ||
| - Actions are attributed to the **commit author** (matched to a member by email). If the git email isn't a member, the commit is still linked but smart actions are skipped. | ||
| - Actions are attributed to the **commit author** (matched by GitHub login first, then email). If neither maps to a member, the commit is still linked but smart actions are skipped. | ||
| - Actions run **once per commit** and only for commits authored **after** the repo was connected — a first import of history never retroactively closes tasks or logs time. | ||
@@ -100,6 +148,8 @@ - Any message that *contains* the syntax triggers it — even one describing it. Don't put example directives in real commit messages. | ||
| ## Attribution | ||
| Everything you do via this server is flagged \`viaApi\` and shows an **AI** badge in the app. Smart-commit actions are attributed to the commit author, not the AI.`; | ||
| Everything you do via this server is flagged \`viaApi\` and shows an **AI** badge in the app — on tasks, on time entries, and in a document's history. Smart-commit actions are attributed to the commit author, not the AI. | ||
| That badge is the reason to be careful with edits rather than a licence to be careless: people read a document as something a colleague wrote, and the badge is on the version, not on the page.`; | ||
| const server = new McpServer( | ||
| { name: "timewave", version: "0.3.0" }, | ||
| { name: "timewave", version: "0.4.0" }, | ||
| { instructions: INSTRUCTIONS }, | ||
@@ -347,4 +397,204 @@ ); | ||
| // --- who am I ------------------------------------------------------------- | ||
| server.registerTool( | ||
| "whoami", | ||
| { | ||
| title: "Who this key belongs to", | ||
| description: | ||
| "The person and organization behind this key, and the org's default currency. Useful before writing anything: it says whose name will be on it.", | ||
| }, | ||
| tool(() => call("GET", "/me")), | ||
| ); | ||
| // --- checklists ----------------------------------------------------------- | ||
| // | ||
| // A task's subtasks. Without these an assistant can write a checklist when it | ||
| // creates a task and then never tick anything off, which is how a board ends up | ||
| // showing 0/6 on work that is finished. | ||
| server.registerTool( | ||
| "add_checklist_item", | ||
| { | ||
| title: "Add a checklist item", | ||
| description: "Add a subtask to a task's checklist. Returns the whole task.", | ||
| inputSchema: { id: z.string().describe("Task id"), text: z.string() }, | ||
| }, | ||
| tool(({ id, text }) => call("POST", `/tasks/${id}/checklist`, { text })), | ||
| ); | ||
| server.registerTool( | ||
| "set_checklist_item", | ||
| { | ||
| title: "Tick a checklist item off (or back on)", | ||
| description: | ||
| "Mark one checklist item done or not done. Get itemId from get_task. Returns the whole task.", | ||
| inputSchema: { | ||
| id: z.string().describe("Task id"), | ||
| itemId: z.string(), | ||
| done: z.boolean().optional().describe("Defaults to true"), | ||
| }, | ||
| }, | ||
| tool(({ id, itemId, done }) => | ||
| call("PATCH", `/tasks/${id}/checklist/${itemId}`, { done: done ?? true }), | ||
| ), | ||
| ); | ||
| server.registerTool( | ||
| "remove_checklist_item", | ||
| { | ||
| title: "Remove a checklist item", | ||
| description: "Delete one item from a task's checklist. Returns the whole task.", | ||
| inputSchema: { id: z.string().describe("Task id"), itemId: z.string() }, | ||
| }, | ||
| tool(({ id, itemId }) => call("DELETE", `/tasks/${id}/checklist/${itemId}`)), | ||
| ); | ||
| // --- timesheets ----------------------------------------------------------- | ||
| server.registerTool( | ||
| "list_timesheets", | ||
| { | ||
| title: "Recent weeks and whether they are handed in", | ||
| description: | ||
| "Your last few weeks: total tracked, status (OPEN | SUBMITTED | APPROVED | REJECTED), and whether the week is locked. Includes weeks nobody has submitted.", | ||
| inputSchema: { limit: z.number().optional().describe("How many weeks back, counting this one (default 8)") }, | ||
| }, | ||
| tool(({ limit }) => call("GET", `/timesheets${limit != null ? `?limit=${limit}` : ""}`)), | ||
| ); | ||
| server.registerTool( | ||
| "get_timesheet", | ||
| { | ||
| title: "One week's timesheet", | ||
| description: | ||
| "A week day by day and project by project, with its status. `week` is any date inside the week — the server normalises it to the Monday.", | ||
| inputSchema: { week: z.string().describe("ISO date inside the week, e.g. 2026-08-06") }, | ||
| }, | ||
| tool(({ week }) => call("GET", `/timesheets/${week}`)), | ||
| ); | ||
| server.registerTool( | ||
| "submit_timesheet", | ||
| { | ||
| title: "Hand a week in", | ||
| description: | ||
| "Submit a week for approval. This LOCKS it: no time can be added or edited in that week afterwards. Ask the person first — it is their week and their statement about it.", | ||
| inputSchema: { week: z.string().describe("ISO date inside the week") }, | ||
| }, | ||
| tool(({ week }) => call("POST", `/timesheets/${week}/submit`)), | ||
| ); | ||
| server.registerTool( | ||
| "withdraw_timesheet", | ||
| { | ||
| title: "Take a submitted week back", | ||
| description: | ||
| "Unlock a week that was handed in but not yet reviewed. An approved week cannot be withdrawn — an admin has to reopen it.", | ||
| inputSchema: { week: z.string().describe("ISO date inside the week") }, | ||
| }, | ||
| tool(({ week }) => call("POST", `/timesheets/${week}/withdraw`)), | ||
| ); | ||
| // --- knowledge base ------------------------------------------------------- | ||
| // | ||
| // Closed to assistants until an owner opens it in Settings → Knowledge base, | ||
| // and a key needs the `docs` scope. Individual pages can be held back, and a | ||
| // held-back page reads exactly like one that does not exist. That is by design; | ||
| // do not treat a "not found" here as a bug to route around. | ||
| server.registerTool( | ||
| "list_docs", | ||
| { | ||
| title: "Search the knowledge base", | ||
| description: | ||
| "Documents you can see, newest first, without their text. `q` matches the title. Use `parentId: \"root\"` for the top of the tree.", | ||
| inputSchema: { | ||
| q: z.string().optional().describe("Match on the title"), | ||
| projectId: z.string().optional(), | ||
| parentId: z.string().optional().describe('A document id, or "root" for top-level pages'), | ||
| limit: z.number().optional().describe("Default 50, max 200"), | ||
| }, | ||
| }, | ||
| tool(({ q, projectId, parentId, limit }) => { | ||
| const p = new URLSearchParams(); | ||
| if (q) p.set("q", q); | ||
| if (projectId) p.set("projectId", projectId); | ||
| if (parentId) p.set("parentId", parentId); | ||
| if (limit != null) p.set("limit", String(limit)); | ||
| const qs = p.toString(); | ||
| return call("GET", `/docs${qs ? `?${qs}` : ""}`); | ||
| }), | ||
| ); | ||
| server.registerTool( | ||
| "get_doc", | ||
| { | ||
| title: "Read a document", | ||
| description: "One document with its Markdown, who may see it, and its attachments.", | ||
| inputSchema: { id: z.string() }, | ||
| }, | ||
| tool(({ id }) => call("GET", `/docs/${id}`)), | ||
| ); | ||
| server.registerTool( | ||
| "create_doc", | ||
| { | ||
| title: "Write a new document", | ||
| description: | ||
| "Create a page. PRIVATE unless you say otherwise — which means only its author will see it, so say `visibility: \"ORG\"` when you are writing something for the team.", | ||
| inputSchema: { | ||
| title: z.string(), | ||
| body: z.string().optional().describe("Markdown"), | ||
| visibility: z.enum(["PRIVATE", "RESTRICTED", "ORG"]).optional(), | ||
| kind: z.enum(["REFERENCE", "WIKI"]).optional(), | ||
| projectId: z.string().optional(), | ||
| parentId: z.string().optional().describe("File it under another document"), | ||
| }, | ||
| }, | ||
| tool((args) => call("POST", "/docs", args)), | ||
| ); | ||
| server.registerTool( | ||
| "update_doc", | ||
| { | ||
| title: "Edit a document", | ||
| description: | ||
| "Change the title, the body, or who may see it. Only the fields you send are touched, so renaming does not need the body. The previous text is kept as a version — but a person still has to go and find it, so prefer editing the part that is wrong over replacing the page.", | ||
| inputSchema: { | ||
| id: z.string(), | ||
| title: z.string().optional(), | ||
| body: z.string().optional().describe("The full Markdown, replacing what is there"), | ||
| visibility: z.enum(["PRIVATE", "RESTRICTED", "ORG"]).optional().describe("Author only"), | ||
| }, | ||
| }, | ||
| tool(({ id, ...patch }) => call("PATCH", `/docs/${id}`, patch)), | ||
| ); | ||
| server.registerTool( | ||
| "list_doc_versions", | ||
| { | ||
| title: "What a document said before", | ||
| description: | ||
| "The document's history, newest first, without the old text: who changed it, when, whether it was an assistant, and how long each version was.", | ||
| inputSchema: { id: z.string(), limit: z.number().optional() }, | ||
| }, | ||
| tool(({ id, limit }) => | ||
| call("GET", `/docs/${id}/versions${limit != null ? `?limit=${limit}` : ""}`), | ||
| ), | ||
| ); | ||
| server.registerTool( | ||
| "get_doc_version", | ||
| { | ||
| title: "Read one old version", | ||
| description: | ||
| "An earlier version in full. To restore it, read it and send its text back through update_doc.", | ||
| inputSchema: { id: z.string().describe("Document id"), versionId: z.string() }, | ||
| }, | ||
| tool(({ id, versionId }) => call("GET", `/docs/${id}/versions/${versionId}`)), | ||
| ); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error(`TimeWave MCP server connected to ${BASE}`); |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
31017
66.58%535
72.03%168
25.37%