@tenonhq/dovetail-mcp 0.0.2 → 0.0.5

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 CHANGED
@@ -1,8 +1,15 @@
1
1
  # @tenonhq/dovetail-mcp
2
2
 
3
- MCP server exposing **read-only** ClickUp / Gmail / Google Calendar / ServiceNow
4
- tools backed by the existing Dovetail integration packages. Phase 1 — write
5
- operations are deliberately out of scope (see PRD).
3
+ MCP server exposing read tools for ClickUp / Gmail / Google Calendar / ServiceNow
4
+ plus **gated Phase-2 ClickUp writes**, backed by the existing Dovetail
5
+ integration packages. Gmail / Calendar / ServiceNow remain read-only.
6
+
7
+ ClickUp writes are gated by the **operator-controlled** `SINC_MCP_WRITES_ENABLE=1`
8
+ env flag — off by default, the server cannot write at all. When the flag is on,
9
+ each write call also requires `confirm:true`; without it the tool returns a
10
+ dry-run preview. `confirm:true` is a preview/speed-bump (the calling agent can
11
+ set it itself), not a human-in-the-loop checkpoint — the env flag is the real
12
+ boundary.
6
13
 
7
14
  ## Install
8
15
 
@@ -32,16 +39,18 @@ clear "not configured" error.
32
39
  | `SINC_MCP_SN_TABLE_OVERRIDE`| Allow specific denied tables (comma-separated) |
33
40
  | `SINC_MCP_TELEMETRY_DISABLE`| `1`/`true` to skip telemetry writes |
34
41
  | `SINC_MCP_TELEMETRY_PATH` | Override `~/.dovetail-mcp/telemetry.jsonl` |
42
+ | `SINC_MCP_WRITES_ENABLE` | `1` to enable the gated ClickUp write tools (off by default) |
35
43
 
36
44
  > **OAuth scope warning:** Dovetail's existing Google refresh tokens are
37
45
  > issued with **write-capable** scopes (`gmail.modify`, `calendar`).
38
- > dovetail-mcp enforces read-only at the handler level (we never import the
39
- > upstream write functions and ESLint blocks new such imports), but the token
40
- > itself can write Gmail/Calendar. Re-running `dovetail-google-auth` setup
46
+ > dovetail-mcp never imports Gmail/Calendar write functions (ESLint + the static
47
+ > scan block them), so it cannot write Gmail/Calendar even though the token
48
+ > could. (ClickUp writes are the one allowed write surface see below.)
49
+ > Re-running `dovetail-google-auth` setup
41
50
  > with `gmail.readonly` + `calendar.readonly` scopes is recommended for
42
51
  > defence-in-depth and is tracked separately.
43
52
 
44
- ## Tools (12)
53
+ ## Tools (16)
45
54
 
46
55
  | Tool | Purpose |
47
56
  |---------------------------------|------------------------------------------------|
@@ -49,6 +58,10 @@ clear "not configured" error.
49
58
  | `clickup_get_task` | Fetch a single task by ID |
50
59
  | `clickup_search_tasks` | Substring search across team tasks |
51
60
  | `clickup_get_team_sync` | 7-stage pipeline JSON (Blocked → Ready for Release) |
61
+ | `clickup_update_task` 🔒 | **Gated write** — update name/markdown/status/priority |
62
+ | `clickup_set_custom_field` 🔒 | **Gated write** — set one custom-field value |
63
+ | `clickup_create_task` 🔒 | **Gated write** — create a task in a list |
64
+ | `clickup_link_tasks` 🔒 | **Gated write** — link two tasks |
52
65
  | `gmail_get_unread` | Unread inbox emails |
53
66
  | `gmail_get_starred` | Starred emails |
54
67
  | `gmail_search` | Gmail query syntax |
@@ -62,6 +75,14 @@ ServiceNow deny-list (default): `sys_user_password`, `sys_user_token`,
62
75
  `sys_credential`, `sys_secret`, `sys_user_grmember`, `sys_audit`. Override
63
76
  per-table with `SINC_MCP_SN_TABLE_OVERRIDE=table_a,table_b`.
64
77
 
78
+ 🔒 **Gated writes (Phase 2).** The four ClickUp write tools are inert unless
79
+ `SINC_MCP_WRITES_ENABLE=1` is set — that's the operator-controlled gate. When
80
+ on, calls return a dry-run preview unless `confirm:true` is passed; the
81
+ `confirm` flag is a preview affordance, not a human checkpoint (the calling
82
+ agent can set it itself). Target a custom ID (e.g. `DEV-225`) with
83
+ `customTaskIds:true` + `teamId`. Gmail, Calendar, and ServiceNow stay
84
+ read-only.
85
+
65
86
  ## Run
66
87
 
67
88
  ```bash
@@ -141,19 +162,29 @@ Every tool call appends one JSON line to `~/.dovetail-mcp/telemetry.jsonl`
141
162
  Disable with `SINC_MCP_TELEMETRY_DISABLE=1`. Override the path with
142
163
  `SINC_MCP_TELEMETRY_PATH=/tmp/foo.jsonl`. Rotate manually for v1.
143
164
 
144
- ## Read-only enforcement
165
+ ## Write-surface enforcement
145
166
 
146
- Three layers:
167
+ Writes are confined to **one declared module** — `src/tools/clickup-write.ts` —
168
+ which may use only the ClickUp write functions. Every other tool module stays
169
+ read-only. Three layers enforce this:
147
170
 
148
- 1. **Imports.** Tool modules import only the read functions from each upstream
149
- Dovetail package; write functions are never imported.
150
- 2. **ESLint** (`.eslintrc.json` `no-restricted-imports`) blocks import of any
151
- write function from `dovetail-clickup` / `dovetail-gmail` /
152
- `dovetail-google-calendar` / `dovetail-servicenow`.
171
+ 1. **Imports.** Read tool modules import only read functions; the write module
172
+ imports only `createTask` / `updateTask` / `setCustomField` / `linkTask`.
173
+ 2. **ESLint** (`.eslintrc.json` `no-restricted-imports`) blocks write imports
174
+ from `dovetail-clickup` / `dovetail-gmail` / `dovetail-google-calendar` /
175
+ `dovetail-servicenow`, with a scoped `overrides` entry that permits the
176
+ ClickUp writes **only** in `clickup-write.ts` (Gmail/Calendar/SN still banned
177
+ even there).
153
178
  3. **Static scan test** (`src/tests/readonly-imports.test.ts`) reads every
154
- `src/tools/*.ts` and asserts no occurrence of forbidden symbols, including
155
- `client.claude.*` (the ServiceNow write namespace, which can't be blocked
156
- at the import level since it's a property access).
179
+ `src/tools/*.ts` and asserts no forbidden write symbol appears including
180
+ `client.claude.*` (the ServiceNow write namespace). The lone exception is the
181
+ declared write module's ClickUp allowlist (`WRITE_MODULE_ALLOW`); a new file
182
+ cannot opt itself in.
183
+
184
+ At runtime, writes are gated by the operator-controlled `SINC_MCP_WRITES_ENABLE=1`
185
+ flag (see "Gated writes" above). A per-call `confirm:true` switches the tool
186
+ from preview to apply; it is *not* a human checkpoint — the calling agent can
187
+ satisfy it itself, so it functions as a preview affordance, not a second factor.
157
188
 
158
189
  ## Troubleshooting
159
190
 
package/dist/config.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface SincMcpConfig {
24
24
  clickup?: ClickUpConfig;
25
25
  google?: GoogleConfig;
26
26
  servicenowSafety: ServiceNowSafetyConfig;
27
+ writesEnabled: boolean;
27
28
  }
28
29
  export interface ConfigLoadResult {
29
30
  config: SincMcpConfig;
package/dist/config.js CHANGED
@@ -41,7 +41,8 @@ function loadConfig() {
41
41
  config: {
42
42
  clickup: clickup,
43
43
  google: google,
44
- servicenowSafety: loadServiceNowSafety()
44
+ servicenowSafety: loadServiceNowSafety(),
45
+ writesEnabled: process.env.SINC_MCP_WRITES_ENABLE === "1"
45
46
  },
46
47
  missing: { clickup: clickupMissing, google: googleMissing }
47
48
  };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @tenonhq/dovetail-mcp
3
3
  *
4
- * MCP server exposing read-only tools for ClickUp, Gmail, Google Calendar,
5
- * and ServiceNow, backed by the existing Dovetail integration packages.
4
+ * MCP server exposing read tools for ClickUp, Gmail, Google Calendar, and
5
+ * ServiceNow, plus gated Phase-2 ClickUp write tools, backed by the existing
6
+ * Dovetail integration packages.
6
7
  *
7
8
  * Library exports here are consumed by tests and by alternate hosts
8
9
  * (e.g. an HTTP transport in the future). The bin entry lives in server.ts.
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * @tenonhq/dovetail-mcp
4
4
  *
5
- * MCP server exposing read-only tools for ClickUp, Gmail, Google Calendar,
6
- * and ServiceNow, backed by the existing Dovetail integration packages.
5
+ * MCP server exposing read tools for ClickUp, Gmail, Google Calendar, and
6
+ * ServiceNow, plus gated Phase-2 ClickUp write tools, backed by the existing
7
+ * Dovetail integration packages.
7
8
  *
8
9
  * Library exports here are consumed by tests and by alternate hosts
9
10
  * (e.g. an HTTP transport in the future). The bin entry lives in server.ts.
@@ -37,7 +38,10 @@ function buildDepsFromEnv() {
37
38
  missingDescription: missingDescription
38
39
  };
39
40
  if (loaded.config.clickup) {
40
- deps.clickup = { config: loaded.config.clickup };
41
+ deps.clickup = {
42
+ config: loaded.config.clickup,
43
+ writesEnabled: loaded.config.writesEnabled
44
+ };
41
45
  }
42
46
  if (loaded.config.google) {
43
47
  deps.gmail = { config: loaded.config.google };
@@ -13,7 +13,7 @@ import type { ClickUpDeps } from "./tools/clickup";
13
13
  import type { GmailDeps } from "./tools/gmail";
14
14
  import type { CalendarDeps } from "./tools/calendar";
15
15
  import type { ServiceNowDeps } from "./tools/servicenow";
16
- export declare var TOOL_NAMES: readonly ["clickup_list_tasks", "clickup_get_task", "clickup_search_tasks", "clickup_get_team_sync", "gmail_get_unread", "gmail_get_starred", "gmail_search", "gmail_get_action_required", "calendar_get_today", "calendar_get_week", "calendar_get_event", "servicenow_query_table"];
16
+ export declare var TOOL_NAMES: readonly ["clickup_list_tasks", "clickup_get_task", "clickup_search_tasks", "clickup_get_team_sync", "clickup_update_task", "clickup_set_custom_field", "clickup_create_task", "clickup_link_tasks", "gmail_get_unread", "gmail_get_starred", "gmail_search", "gmail_get_action_required", "calendar_get_today", "calendar_get_week", "calendar_get_event", "servicenow_query_table"];
17
17
  export type ToolName = typeof TOOL_NAMES[number];
18
18
  export interface RegistryDeps {
19
19
  clickup?: ClickUpDeps;
package/dist/registry.js CHANGED
@@ -19,6 +19,7 @@ const gmail_1 = require("./schemas/gmail");
19
19
  const calendar_1 = require("./schemas/calendar");
20
20
  const servicenow_1 = require("./schemas/servicenow");
21
21
  const clickup_2 = require("./tools/clickup");
22
+ const clickup_write_1 = require("./tools/clickup-write");
22
23
  const gmail_2 = require("./tools/gmail");
23
24
  const calendar_2 = require("./tools/calendar");
24
25
  const servicenow_2 = require("./tools/servicenow");
@@ -27,6 +28,10 @@ exports.TOOL_NAMES = [
27
28
  "clickup_get_task",
28
29
  "clickup_search_tasks",
29
30
  "clickup_get_team_sync",
31
+ "clickup_update_task",
32
+ "clickup_set_custom_field",
33
+ "clickup_create_task",
34
+ "clickup_link_tasks",
30
35
  "gmail_get_unread",
31
36
  "gmail_get_starred",
32
37
  "gmail_search",
@@ -72,6 +77,38 @@ function buildDescriptors(deps) {
72
77
  return (0, clickup_2.clickupGetTeamSync)(args, deps.clickup);
73
78
  })
74
79
  },
80
+ {
81
+ name: "clickup_update_task",
82
+ description: "Gated write: update a ClickUp task (name, markdownContent, status, priority). Requires SINC_MCP_WRITES_ENABLE=1; returns a dry-run preview unless confirm:true. Use customTaskIds:true + teamId to target a custom ID like DEV-225.",
83
+ shape: clickup_1.clickupUpdateTaskSchema.shape,
84
+ handler: requireConfig(clickupReady, "ClickUp", deps.missingDescription, function (args) {
85
+ return (0, clickup_write_1.clickupUpdateTask)(args, deps.clickup);
86
+ })
87
+ },
88
+ {
89
+ name: "clickup_set_custom_field",
90
+ description: "Gated write: set one custom-field value on a task (POST /task/{id}/field/{fieldId}). Requires SINC_MCP_WRITES_ENABLE=1; dry-run unless confirm:true. value shape: string for text/url, option id for drop_down, { add:[], rem:[] } for users.",
91
+ shape: clickup_1.clickupSetCustomFieldSchema.shape,
92
+ handler: requireConfig(clickupReady, "ClickUp", deps.missingDescription, function (args) {
93
+ return (0, clickup_write_1.clickupSetCustomField)(args, deps.clickup);
94
+ })
95
+ },
96
+ {
97
+ name: "clickup_create_task",
98
+ description: "Gated write: create a ClickUp task in a list (markdownContent, status, priority, assignees, customFields). Requires SINC_MCP_WRITES_ENABLE=1; dry-run unless confirm:true.",
99
+ shape: clickup_1.clickupCreateTaskSchema.shape,
100
+ handler: requireConfig(clickupReady, "ClickUp", deps.missingDescription, function (args) {
101
+ return (0, clickup_write_1.clickupCreateTask)(args, deps.clickup);
102
+ })
103
+ },
104
+ {
105
+ name: "clickup_link_tasks",
106
+ description: "Gated write: link two ClickUp tasks (POST /task/{id}/link/{linksTo}). Requires SINC_MCP_WRITES_ENABLE=1; dry-run unless confirm:true. Use customTaskIds:true + teamId for custom IDs.",
107
+ shape: clickup_1.clickupLinkTasksSchema.shape,
108
+ handler: requireConfig(clickupReady, "ClickUp", deps.missingDescription, function (args) {
109
+ return (0, clickup_write_1.clickupLinkTasks)(args, deps.clickup);
110
+ })
111
+ },
75
112
  {
76
113
  name: "gmail_get_unread",
77
114
  description: "Fetch unread emails from the inbox.",
@@ -43,3 +43,119 @@ export type ClickupListTasksInput = z.infer<typeof clickupListTasksSchema>;
43
43
  export type ClickupGetTaskInput = z.infer<typeof clickupGetTaskSchema>;
44
44
  export type ClickupSearchTasksInput = z.infer<typeof clickupSearchTasksSchema>;
45
45
  export type ClickupGetTeamSyncInput = z.infer<typeof clickupGetTeamSyncSchema>;
46
+ export declare var clickupUpdateTaskSchema: z.ZodObject<{
47
+ taskId: z.ZodString;
48
+ name: z.ZodOptional<z.ZodString>;
49
+ markdownContent: z.ZodOptional<z.ZodString>;
50
+ status: z.ZodOptional<z.ZodString>;
51
+ priority: z.ZodOptional<z.ZodNumber>;
52
+ customTaskIds: z.ZodOptional<z.ZodBoolean>;
53
+ teamId: z.ZodOptional<z.ZodString>;
54
+ confirm: z.ZodOptional<z.ZodBoolean>;
55
+ }, "strict", z.ZodTypeAny, {
56
+ taskId: string;
57
+ name?: string | undefined;
58
+ priority?: number | undefined;
59
+ status?: string | undefined;
60
+ confirm?: boolean | undefined;
61
+ teamId?: string | undefined;
62
+ markdownContent?: string | undefined;
63
+ customTaskIds?: boolean | undefined;
64
+ }, {
65
+ taskId: string;
66
+ name?: string | undefined;
67
+ priority?: number | undefined;
68
+ status?: string | undefined;
69
+ confirm?: boolean | undefined;
70
+ teamId?: string | undefined;
71
+ markdownContent?: string | undefined;
72
+ customTaskIds?: boolean | undefined;
73
+ }>;
74
+ export declare var clickupSetCustomFieldSchema: z.ZodObject<{
75
+ taskId: z.ZodString;
76
+ fieldId: z.ZodString;
77
+ value: z.ZodUnknown;
78
+ customTaskIds: z.ZodOptional<z.ZodBoolean>;
79
+ teamId: z.ZodOptional<z.ZodString>;
80
+ confirm: z.ZodOptional<z.ZodBoolean>;
81
+ }, "strict", z.ZodTypeAny, {
82
+ taskId: string;
83
+ fieldId: string;
84
+ value?: unknown;
85
+ confirm?: boolean | undefined;
86
+ teamId?: string | undefined;
87
+ customTaskIds?: boolean | undefined;
88
+ }, {
89
+ taskId: string;
90
+ fieldId: string;
91
+ value?: unknown;
92
+ confirm?: boolean | undefined;
93
+ teamId?: string | undefined;
94
+ customTaskIds?: boolean | undefined;
95
+ }>;
96
+ export declare var clickupCreateTaskSchema: z.ZodObject<{
97
+ listId: z.ZodString;
98
+ name: z.ZodString;
99
+ markdownContent: z.ZodOptional<z.ZodString>;
100
+ status: z.ZodOptional<z.ZodString>;
101
+ priority: z.ZodOptional<z.ZodNumber>;
102
+ assignees: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
103
+ customFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
104
+ id: z.ZodString;
105
+ value: z.ZodUnknown;
106
+ }, "strip", z.ZodTypeAny, {
107
+ id: string;
108
+ value?: unknown;
109
+ }, {
110
+ id: string;
111
+ value?: unknown;
112
+ }>, "many">>;
113
+ confirm: z.ZodOptional<z.ZodBoolean>;
114
+ }, "strict", z.ZodTypeAny, {
115
+ name: string;
116
+ listId: string;
117
+ priority?: number | undefined;
118
+ status?: string | undefined;
119
+ confirm?: boolean | undefined;
120
+ markdownContent?: string | undefined;
121
+ assignees?: number[] | undefined;
122
+ customFields?: {
123
+ id: string;
124
+ value?: unknown;
125
+ }[] | undefined;
126
+ }, {
127
+ name: string;
128
+ listId: string;
129
+ priority?: number | undefined;
130
+ status?: string | undefined;
131
+ confirm?: boolean | undefined;
132
+ markdownContent?: string | undefined;
133
+ assignees?: number[] | undefined;
134
+ customFields?: {
135
+ id: string;
136
+ value?: unknown;
137
+ }[] | undefined;
138
+ }>;
139
+ export declare var clickupLinkTasksSchema: z.ZodObject<{
140
+ taskId: z.ZodString;
141
+ linksTo: z.ZodString;
142
+ customTaskIds: z.ZodOptional<z.ZodBoolean>;
143
+ teamId: z.ZodOptional<z.ZodString>;
144
+ confirm: z.ZodOptional<z.ZodBoolean>;
145
+ }, "strict", z.ZodTypeAny, {
146
+ taskId: string;
147
+ linksTo: string;
148
+ confirm?: boolean | undefined;
149
+ teamId?: string | undefined;
150
+ customTaskIds?: boolean | undefined;
151
+ }, {
152
+ taskId: string;
153
+ linksTo: string;
154
+ confirm?: boolean | undefined;
155
+ teamId?: string | undefined;
156
+ customTaskIds?: boolean | undefined;
157
+ }>;
158
+ export type ClickupUpdateTaskInput = z.infer<typeof clickupUpdateTaskSchema>;
159
+ export type ClickupSetCustomFieldInput = z.infer<typeof clickupSetCustomFieldSchema>;
160
+ export type ClickupCreateTaskInput = z.infer<typeof clickupCreateTaskSchema>;
161
+ export type ClickupLinkTasksInput = z.infer<typeof clickupLinkTasksSchema>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.clickupGetTeamSyncSchema = exports.clickupSearchTasksSchema = exports.clickupGetTaskSchema = exports.clickupListTasksSchema = void 0;
3
+ exports.clickupLinkTasksSchema = exports.clickupCreateTaskSchema = exports.clickupSetCustomFieldSchema = exports.clickupUpdateTaskSchema = exports.clickupGetTeamSyncSchema = exports.clickupSearchTasksSchema = exports.clickupGetTaskSchema = exports.clickupListTasksSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  exports.clickupListTasksSchema = zod_1.z.object({
6
6
  teamId: zod_1.z.string().min(1).optional(),
@@ -18,3 +18,45 @@ exports.clickupSearchTasksSchema = zod_1.z.object({
18
18
  exports.clickupGetTeamSyncSchema = zod_1.z.object({
19
19
  teamId: zod_1.z.string().min(1).optional()
20
20
  }).strict();
21
+ // --- Phase-2 gated write schemas ---
22
+ // Every write carries an optional confirm flag. Without confirm:true the tool
23
+ // returns a dry-run preview; with it, the write executes. customTaskIds:true
24
+ // treats taskId as a custom ID (e.g. "DEV-225") and requires teamId — that
25
+ // cross-field rule is enforced in clickup-write.ts (it can't live on the
26
+ // schema because the MCP SDK consumes .shape, which a ZodEffects from
27
+ // .refine() doesn't expose).
28
+ exports.clickupUpdateTaskSchema = zod_1.z.object({
29
+ taskId: zod_1.z.string().min(1),
30
+ name: zod_1.z.string().min(1).optional(),
31
+ markdownContent: zod_1.z.string().optional(),
32
+ status: zod_1.z.string().min(1).optional(),
33
+ priority: zod_1.z.number().int().min(1).max(4).optional(),
34
+ customTaskIds: zod_1.z.boolean().optional(),
35
+ teamId: zod_1.z.string().min(1).optional(),
36
+ confirm: zod_1.z.boolean().optional()
37
+ }).strict();
38
+ exports.clickupSetCustomFieldSchema = zod_1.z.object({
39
+ taskId: zod_1.z.string().min(1),
40
+ fieldId: zod_1.z.string().min(1),
41
+ value: zod_1.z.unknown(),
42
+ customTaskIds: zod_1.z.boolean().optional(),
43
+ teamId: zod_1.z.string().min(1).optional(),
44
+ confirm: zod_1.z.boolean().optional()
45
+ }).strict();
46
+ exports.clickupCreateTaskSchema = zod_1.z.object({
47
+ listId: zod_1.z.string().min(1),
48
+ name: zod_1.z.string().min(1),
49
+ markdownContent: zod_1.z.string().optional(),
50
+ status: zod_1.z.string().min(1).optional(),
51
+ priority: zod_1.z.number().int().min(1).max(4).optional(),
52
+ assignees: zod_1.z.array(zod_1.z.number()).optional(),
53
+ customFields: zod_1.z.array(zod_1.z.object({ id: zod_1.z.string().min(1), value: zod_1.z.unknown() })).optional(),
54
+ confirm: zod_1.z.boolean().optional()
55
+ }).strict();
56
+ exports.clickupLinkTasksSchema = zod_1.z.object({
57
+ taskId: zod_1.z.string().min(1),
58
+ linksTo: zod_1.z.string().min(1),
59
+ customTaskIds: zod_1.z.boolean().optional(),
60
+ teamId: zod_1.z.string().min(1).optional(),
61
+ confirm: zod_1.z.boolean().optional()
62
+ }).strict();
package/dist/server.js CHANGED
File without changes
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ClickUp gated write tools (Phase 2).
3
+ *
4
+ * This is the ONE declared write module. It is the single exception in
5
+ * tests/readonly-imports.test.ts and .eslintrc.json — every OTHER tool module
6
+ * must remain read-only.
7
+ *
8
+ * Gating:
9
+ * 1. Operator gate: deps.writesEnabled (SINC_MCP_WRITES_ENABLE=1), else
10
+ * refuse. This is the only boundary an autonomous agent cannot self-flip.
11
+ * 2. Preview affordance: confirm:true in the args, else return a dry-run.
12
+ * The calling agent can set confirm itself — this is a preview/speed-bump,
13
+ * NOT a human-in-the-loop checkpoint.
14
+ */
15
+ import { ClickUpDeps } from "./clickup";
16
+ import { ClickupUpdateTaskInput, ClickupSetCustomFieldInput, ClickupCreateTaskInput, ClickupLinkTasksInput } from "../schemas/clickup";
17
+ export declare function clickupUpdateTask(args: ClickupUpdateTaskInput, deps: ClickUpDeps): Promise<any>;
18
+ export declare function clickupSetCustomField(args: ClickupSetCustomFieldInput, deps: ClickUpDeps): Promise<any>;
19
+ export declare function clickupCreateTask(args: ClickupCreateTaskInput, deps: ClickUpDeps): Promise<any>;
20
+ export declare function clickupLinkTasks(args: ClickupLinkTasksInput, deps: ClickUpDeps): Promise<any>;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * ClickUp gated write tools (Phase 2).
4
+ *
5
+ * This is the ONE declared write module. It is the single exception in
6
+ * tests/readonly-imports.test.ts and .eslintrc.json — every OTHER tool module
7
+ * must remain read-only.
8
+ *
9
+ * Gating:
10
+ * 1. Operator gate: deps.writesEnabled (SINC_MCP_WRITES_ENABLE=1), else
11
+ * refuse. This is the only boundary an autonomous agent cannot self-flip.
12
+ * 2. Preview affordance: confirm:true in the args, else return a dry-run.
13
+ * The calling agent can set confirm itself — this is a preview/speed-bump,
14
+ * NOT a human-in-the-loop checkpoint.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.clickupUpdateTask = clickupUpdateTask;
18
+ exports.clickupSetCustomField = clickupSetCustomField;
19
+ exports.clickupCreateTask = clickupCreateTask;
20
+ exports.clickupLinkTasks = clickupLinkTasks;
21
+ const dovetail_clickup_1 = require("@tenonhq/dovetail-clickup");
22
+ const clickup_1 = require("./clickup");
23
+ function ensureWritesEnabled(deps) {
24
+ if (!deps.writesEnabled) {
25
+ throw new Error("ClickUp writes are disabled. Set SINC_MCP_WRITES_ENABLE=1 to enable gated writes.");
26
+ }
27
+ }
28
+ /**
29
+ * Cross-field guard: customTaskIds:true requires teamId. ClickUp would
30
+ * otherwise return an opaque 401 (the same one this PR series set out to fix).
31
+ * Lives in the handler — not on the schema — because the MCP SDK consumes the
32
+ * raw object shape and would ignore a zod refinement.
33
+ */
34
+ function ensureTeamIdWhenCustom(args) {
35
+ if (args.customTaskIds && (typeof args.teamId !== "string" || args.teamId.length === 0)) {
36
+ throw new Error("teamId is required when customTaskIds is true (ClickUp custom-ID lookups need ?team_id=…).");
37
+ }
38
+ }
39
+ var DRY_RUN_NOTE = "Dry run — no write performed. Re-run with confirm:true to apply.";
40
+ async function clickupUpdateTask(args, deps) {
41
+ ensureWritesEnabled(deps);
42
+ ensureTeamIdWhenCustom(args);
43
+ if (!args.confirm) {
44
+ return {
45
+ dryRun: true,
46
+ action: "update_task",
47
+ taskId: args.taskId,
48
+ changes: {
49
+ name: args.name,
50
+ markdownContent: args.markdownContent,
51
+ status: args.status,
52
+ priority: args.priority
53
+ },
54
+ note: DRY_RUN_NOTE
55
+ };
56
+ }
57
+ var client = (0, clickup_1.resolveClient)(deps);
58
+ return await (0, dovetail_clickup_1.updateTask)({
59
+ client: client,
60
+ taskId: args.taskId,
61
+ name: args.name,
62
+ markdownContent: args.markdownContent,
63
+ status: args.status,
64
+ priority: args.priority,
65
+ customTaskIds: args.customTaskIds,
66
+ teamId: args.teamId
67
+ });
68
+ }
69
+ async function clickupSetCustomField(args, deps) {
70
+ ensureWritesEnabled(deps);
71
+ ensureTeamIdWhenCustom(args);
72
+ if (!args.confirm) {
73
+ return {
74
+ dryRun: true,
75
+ action: "set_custom_field",
76
+ taskId: args.taskId,
77
+ fieldId: args.fieldId,
78
+ value: args.value,
79
+ note: DRY_RUN_NOTE
80
+ };
81
+ }
82
+ var client = (0, clickup_1.resolveClient)(deps);
83
+ await (0, dovetail_clickup_1.setCustomField)({
84
+ client: client,
85
+ taskId: args.taskId,
86
+ fieldId: args.fieldId,
87
+ value: args.value,
88
+ customTaskIds: args.customTaskIds,
89
+ teamId: args.teamId
90
+ });
91
+ return { ok: true, action: "set_custom_field", taskId: args.taskId, fieldId: args.fieldId };
92
+ }
93
+ async function clickupCreateTask(args, deps) {
94
+ ensureWritesEnabled(deps);
95
+ if (!args.confirm) {
96
+ return {
97
+ dryRun: true,
98
+ action: "create_task",
99
+ listId: args.listId,
100
+ name: args.name,
101
+ fields: {
102
+ markdownContent: args.markdownContent,
103
+ status: args.status,
104
+ priority: args.priority,
105
+ assignees: args.assignees,
106
+ customFields: args.customFields
107
+ },
108
+ note: DRY_RUN_NOTE
109
+ };
110
+ }
111
+ var client = (0, clickup_1.resolveClient)(deps);
112
+ var customFields = args.customFields
113
+ ? args.customFields.map(function (cf) {
114
+ return { id: cf.id, value: cf.value };
115
+ })
116
+ : undefined;
117
+ return await (0, dovetail_clickup_1.createTask)({
118
+ client: client,
119
+ listId: args.listId,
120
+ name: args.name,
121
+ markdownContent: args.markdownContent,
122
+ status: args.status,
123
+ priority: args.priority,
124
+ assignees: args.assignees,
125
+ customFields: customFields
126
+ });
127
+ }
128
+ async function clickupLinkTasks(args, deps) {
129
+ ensureWritesEnabled(deps);
130
+ ensureTeamIdWhenCustom(args);
131
+ if (!args.confirm) {
132
+ return {
133
+ dryRun: true,
134
+ action: "link_tasks",
135
+ taskId: args.taskId,
136
+ linksTo: args.linksTo,
137
+ note: DRY_RUN_NOTE
138
+ };
139
+ }
140
+ var client = (0, clickup_1.resolveClient)(deps);
141
+ await (0, dovetail_clickup_1.linkTask)({
142
+ client: client,
143
+ taskId: args.taskId,
144
+ linksTo: args.linksTo,
145
+ customTaskIds: args.customTaskIds,
146
+ teamId: args.teamId
147
+ });
148
+ return { ok: true, action: "link_tasks", taskId: args.taskId, linksTo: args.linksTo };
149
+ }
@@ -12,7 +12,9 @@ import { TeamSyncJson } from "./teamsync";
12
12
  export interface ClickUpDeps {
13
13
  config: ClickUpConfig;
14
14
  clientFactory?: (config: ClickUpConfig) => AxiosInstance;
15
+ writesEnabled?: boolean;
15
16
  }
17
+ export declare function resolveClient(deps: ClickUpDeps): AxiosInstance;
16
18
  export declare function clickupListTasks(args: ClickupListTasksInput, deps: ClickUpDeps): Promise<{
17
19
  tasks: any[];
18
20
  byStatus: Record<string, any[]>;
@@ -7,6 +7,7 @@
7
7
  * (.eslintrc.json) and asserted absent by tests/readonly-imports.test.ts.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.resolveClient = resolveClient;
10
11
  exports.clickupListTasks = clickupListTasks;
11
12
  exports.clickupGetTask = clickupGetTask;
12
13
  exports.clickupSearchTasks = clickupSearchTasks;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-mcp",
3
- "version": "0.0.2",
4
- "description": "MCP server exposing read-only ClickUp / Gmail / Calendar / ServiceNow tools backed by the Dovetail integration packages.",
3
+ "version": "0.0.5",
4
+ "description": "MCP server exposing read tools for ClickUp / Gmail / Calendar / ServiceNow plus gated ClickUp writes, backed by the Dovetail integration packages.",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {
7
7
  "dove-mcp": "./dist/server.js",
@@ -17,7 +17,7 @@
17
17
  "license": "GPL-3.0",
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.29.0",
20
- "@tenonhq/dovetail-clickup": "^0.0.7",
20
+ "@tenonhq/dovetail-clickup": "^0.0.10",
21
21
  "@tenonhq/dovetail-gmail": "^0.0.7",
22
22
  "@tenonhq/dovetail-google-auth": "^0.0.9",
23
23
  "@tenonhq/dovetail-google-calendar": "^0.0.7",
@@ -32,7 +32,7 @@
32
32
  "typescript": "^5.2.2"
33
33
  },
34
34
  "engines": {
35
- "node": ">=20.0.0"
35
+ "node": ">=22"
36
36
  },
37
37
  "files": [
38
38
  "dist"
@@ -42,9 +42,10 @@
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
45
- "url": "git+https://github.com/tenonhq/dovetail.git"
45
+ "url": "git+https://github.com/TenonHQ/Dovetail.git",
46
+ "directory": "packages/mcp"
46
47
  },
47
48
  "bugs": {
48
- "url": "https://github.com/tenonhq/dovetail/issues"
49
+ "url": "https://github.com/TenonHQ/Dovetail/issues"
49
50
  }
50
51
  }