@tenonhq/dovetail-mcp 0.0.1
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 +167 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +93 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +29 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +47 -0
- package/dist/redact.d.ts +8 -0
- package/dist/redact.js +135 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.js +197 -0
- package/dist/schemas/calendar.d.ts +37 -0
- package/dist/schemas/calendar.js +17 -0
- package/dist/schemas/clickup.d.ts +45 -0
- package/dist/schemas/clickup.js +20 -0
- package/dist/schemas/gmail.d.ts +51 -0
- package/dist/schemas/gmail.js +22 -0
- package/dist/schemas/servicenow.d.ts +18 -0
- package/dist/schemas/servicenow.js +10 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +41 -0
- package/dist/telemetry.d.ts +30 -0
- package/dist/telemetry.js +164 -0
- package/dist/tools/calendar.d.ts +18 -0
- package/dist/tools/calendar.js +52 -0
- package/dist/tools/clickup.d.ts +27 -0
- package/dist/tools/clickup.js +75 -0
- package/dist/tools/gmail.d.ts +20 -0
- package/dist/tools/gmail.js +62 -0
- package/dist/tools/servicenow.d.ts +22 -0
- package/dist/tools/servicenow.js +45 -0
- package/dist/tools/teamsync.d.ts +38 -0
- package/dist/tools/teamsync.js +97 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @tenonhq/dovetail-mcp
|
|
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).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @tenonhq/dovetail-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 20 LTS required.
|
|
14
|
+
|
|
15
|
+
## Environment variables
|
|
16
|
+
|
|
17
|
+
The server reads `.env` of the **calling** project (e.g. CTO repo). It boots
|
|
18
|
+
even if some integrations are missing — calls to those tools just return a
|
|
19
|
+
clear "not configured" error.
|
|
20
|
+
|
|
21
|
+
| Variable | Required by |
|
|
22
|
+
|-----------------------------|------------------------------|
|
|
23
|
+
| `CLICKUP_API_TOKEN` | All `clickup_*` tools |
|
|
24
|
+
| `CLICKUP_TEAM_ID` | Optional default for `clickup_list_tasks`/`clickup_search_tasks`/`clickup_get_team_sync` |
|
|
25
|
+
| `GOOGLE_CLIENT_ID` | All `gmail_*` and `calendar_*` tools |
|
|
26
|
+
| `GOOGLE_CLIENT_SECRET` | (same) |
|
|
27
|
+
| `GOOGLE_REFRESH_TOKEN` | (same) |
|
|
28
|
+
| `SN_INSTANCE` (or `SN_DEV_INSTANCE`/`SN_PROD_INSTANCE`) | `servicenow_query_table` |
|
|
29
|
+
| `SN_USER` (or `SN_DEV_USERNAME`/`SN_PROD_USERNAME`) | (same) |
|
|
30
|
+
| `SN_PASSWORD` (or `SN_DEV_PASSWORD`/`SN_PROD_PASSWORD`) | (same) |
|
|
31
|
+
| `SINC_MCP_SN_TABLE_DENY` | Override default deny list (comma-separated) |
|
|
32
|
+
| `SINC_MCP_SN_TABLE_OVERRIDE`| Allow specific denied tables (comma-separated) |
|
|
33
|
+
| `SINC_MCP_TELEMETRY_DISABLE`| `1`/`true` to skip telemetry writes |
|
|
34
|
+
| `SINC_MCP_TELEMETRY_PATH` | Override `~/.dovetail-mcp/telemetry.jsonl` |
|
|
35
|
+
|
|
36
|
+
> **OAuth scope warning:** Dovetail's existing Google refresh tokens are
|
|
37
|
+
> 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
|
|
41
|
+
> with `gmail.readonly` + `calendar.readonly` scopes is recommended for
|
|
42
|
+
> defence-in-depth and is tracked separately.
|
|
43
|
+
|
|
44
|
+
## Tools (12)
|
|
45
|
+
|
|
46
|
+
| Tool | Purpose |
|
|
47
|
+
|---------------------------------|------------------------------------------------|
|
|
48
|
+
| `clickup_list_tasks` | Tasks assigned to the authenticated user |
|
|
49
|
+
| `clickup_get_task` | Fetch a single task by ID |
|
|
50
|
+
| `clickup_search_tasks` | Substring search across team tasks |
|
|
51
|
+
| `clickup_get_team_sync` | 7-stage pipeline JSON (Blocked → Ready for Release) |
|
|
52
|
+
| `gmail_get_unread` | Unread inbox emails |
|
|
53
|
+
| `gmail_get_starred` | Starred emails |
|
|
54
|
+
| `gmail_search` | Gmail query syntax |
|
|
55
|
+
| `gmail_get_action_required` | Unread emails matching action-required patterns|
|
|
56
|
+
| `calendar_get_today` | Today's events |
|
|
57
|
+
| `calendar_get_week` | Next 7 days |
|
|
58
|
+
| `calendar_get_event` | Single event by ID |
|
|
59
|
+
| `servicenow_query_table` | Table API GET (deny-listed tables blocked) |
|
|
60
|
+
|
|
61
|
+
ServiceNow deny-list (default): `sys_user_password`, `sys_user_token`,
|
|
62
|
+
`sys_credential`, `sys_secret`, `sys_user_grmember`, `sys_audit`. Override
|
|
63
|
+
per-table with `SINC_MCP_SN_TABLE_OVERRIDE=table_a,table_b`.
|
|
64
|
+
|
|
65
|
+
## Run
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# stdio transport (intended use under an MCP client like Claude Code)
|
|
69
|
+
npx @tenonhq/dovetail-mcp
|
|
70
|
+
|
|
71
|
+
# Smoke check — list tools and exit, no transport
|
|
72
|
+
npx @tenonhq/dovetail-mcp --smoke
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Wiring into Claude Code (CTO repo)
|
|
76
|
+
|
|
77
|
+
Add the server entry to `.claude/mcp.json` in the consuming project:
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"dovetail": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "@tenonhq/dovetail-mcp"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Until the package is published, point at a local build instead:
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"dovetail": {
|
|
96
|
+
"command": "node",
|
|
97
|
+
"args": ["/absolute/path/to/Dovetail/packages/dovetail-mcp/dist/server.js"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Tools are deferred-by-default — they appear in `ToolSearch` but their schemas
|
|
104
|
+
load on demand. Per the CTO `CLAUDE.md` MCP policy, hoist to always-loaded
|
|
105
|
+
only after telemetry shows >30% session usage.
|
|
106
|
+
|
|
107
|
+
## Agent doc additions
|
|
108
|
+
|
|
109
|
+
Add a one-liner to each consuming agent's prompt:
|
|
110
|
+
|
|
111
|
+
- `task-manager.md`: "ClickUp `clickup_*` MCP tools (under the `dovetail`
|
|
112
|
+
server) are available for live task queries instead of the cron'd
|
|
113
|
+
`context/clickup-tasks.md`."
|
|
114
|
+
- `cto-briefer.md`: "Live data: `gmail_*`, `calendar_*`, `clickup_*` MCP tools
|
|
115
|
+
on the `dovetail` server. Prefer them over the cron'd context markdown when
|
|
116
|
+
freshness matters."
|
|
117
|
+
- `decision-advisor.md`: "Calendar / ClickUp MCP tools on the `dovetail`
|
|
118
|
+
server are available for context-rich reasoning."
|
|
119
|
+
|
|
120
|
+
## Telemetry
|
|
121
|
+
|
|
122
|
+
Every tool call appends one JSON line to `~/.dovetail-mcp/telemetry.jsonl`
|
|
123
|
+
(file mode `0600`, dir mode `0700`):
|
|
124
|
+
|
|
125
|
+
```jsonl
|
|
126
|
+
{"ts":"2026-05-08T17:30:00.000Z","tool":"clickup_list_tasks","args":{"teamId":"123"},"durationMs":420,"success":true}
|
|
127
|
+
{"ts":"2026-05-08T17:30:01.000Z","tool":"gmail_search","args":{"query":"from:alice"},"durationMs":250,"success":true}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Redaction
|
|
131
|
+
|
|
132
|
+
- `body` / `html` / `text` / `content` keys → `"[REDACTED:body]"`.
|
|
133
|
+
- Token-bearing keys (`token`, `password`, `refresh_token`, `access_token`,
|
|
134
|
+
`client_secret`, `apiKey`, `api_key`, `clickup_api_token`, `authorization`,
|
|
135
|
+
`auth`) → `"[REDACTED]"`.
|
|
136
|
+
- Email-shaped strings → `first3***@domain.tld`.
|
|
137
|
+
- Free-form strings >200 chars → `sha256:<first 12 hex>`.
|
|
138
|
+
- Query strings (`query`, `q`, `sysparm_query`, `subjectPatterns`, `labels`,
|
|
139
|
+
`statuses`) are kept verbatim — they're operational signal.
|
|
140
|
+
|
|
141
|
+
Disable with `SINC_MCP_TELEMETRY_DISABLE=1`. Override the path with
|
|
142
|
+
`SINC_MCP_TELEMETRY_PATH=/tmp/foo.jsonl`. Rotate manually for v1.
|
|
143
|
+
|
|
144
|
+
## Read-only enforcement
|
|
145
|
+
|
|
146
|
+
Three layers:
|
|
147
|
+
|
|
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`.
|
|
153
|
+
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).
|
|
157
|
+
|
|
158
|
+
## Troubleshooting
|
|
159
|
+
|
|
160
|
+
- **"ClickUp is not configured — Missing required environment variables …"**
|
|
161
|
+
Set `CLICKUP_API_TOKEN`. The server boots without it; only `clickup_*` calls
|
|
162
|
+
fail until it's set.
|
|
163
|
+
- **"Google authentication failed (… ). Your refresh token may be expired or
|
|
164
|
+
revoked."** Re-run `npm run setup` in `@tenonhq/dovetail-google-auth`.
|
|
165
|
+
- **"Table 'X' is in the default deny list."** Add the table to
|
|
166
|
+
`SINC_MCP_SN_TABLE_OVERRIDE` (comma-separated) only after confirming the
|
|
167
|
+
read is intentional.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregates env-var requirements across the four integrations into a single
|
|
3
|
+
* loadConfig() so users see every missing var at once instead of fixing them
|
|
4
|
+
* one at a time. ServiceNow's createClient() handles its own env precedence
|
|
5
|
+
* (SN_* > SN_DEV_* > SN_PROD_*) — we surface its error verbatim.
|
|
6
|
+
*
|
|
7
|
+
* The ServiceNow deny-list defaults to tables that disclose secrets or audit
|
|
8
|
+
* data on read; users opt in with SINC_MCP_SN_TABLE_OVERRIDE.
|
|
9
|
+
*/
|
|
10
|
+
export interface ClickUpConfig {
|
|
11
|
+
token: string;
|
|
12
|
+
defaultTeamId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GoogleConfig {
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ServiceNowSafetyConfig {
|
|
20
|
+
denyTables: string[];
|
|
21
|
+
overrideTables: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface SincMcpConfig {
|
|
24
|
+
clickup?: ClickUpConfig;
|
|
25
|
+
google?: GoogleConfig;
|
|
26
|
+
servicenowSafety: ServiceNowSafetyConfig;
|
|
27
|
+
}
|
|
28
|
+
export interface ConfigLoadResult {
|
|
29
|
+
config: SincMcpConfig;
|
|
30
|
+
missing: {
|
|
31
|
+
clickup: string[];
|
|
32
|
+
google: string[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function loadConfig(): ConfigLoadResult;
|
|
36
|
+
export declare function formatMissingEnvError(missing: {
|
|
37
|
+
clickup: string[];
|
|
38
|
+
google: string[];
|
|
39
|
+
}): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Aggregates env-var requirements across the four integrations into a single
|
|
4
|
+
* loadConfig() so users see every missing var at once instead of fixing them
|
|
5
|
+
* one at a time. ServiceNow's createClient() handles its own env precedence
|
|
6
|
+
* (SN_* > SN_DEV_* > SN_PROD_*) — we surface its error verbatim.
|
|
7
|
+
*
|
|
8
|
+
* The ServiceNow deny-list defaults to tables that disclose secrets or audit
|
|
9
|
+
* data on read; users opt in with SINC_MCP_SN_TABLE_OVERRIDE.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.loadConfig = loadConfig;
|
|
13
|
+
exports.formatMissingEnvError = formatMissingEnvError;
|
|
14
|
+
var DEFAULT_DENY_TABLES = [
|
|
15
|
+
"sys_user_password",
|
|
16
|
+
"sys_user_token",
|
|
17
|
+
"sys_credential",
|
|
18
|
+
"sys_secret",
|
|
19
|
+
"sys_user_grmember",
|
|
20
|
+
"sys_audit"
|
|
21
|
+
];
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
var clickupMissing = collectClickupMissing();
|
|
24
|
+
var googleMissing = collectGoogleMissing();
|
|
25
|
+
var clickup;
|
|
26
|
+
if (clickupMissing.length === 0) {
|
|
27
|
+
clickup = {
|
|
28
|
+
token: process.env.CLICKUP_API_TOKEN,
|
|
29
|
+
defaultTeamId: process.env.CLICKUP_TEAM_ID || undefined
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var google;
|
|
33
|
+
if (googleMissing.length === 0) {
|
|
34
|
+
google = {
|
|
35
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
36
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
37
|
+
refreshToken: process.env.GOOGLE_REFRESH_TOKEN
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
config: {
|
|
42
|
+
clickup: clickup,
|
|
43
|
+
google: google,
|
|
44
|
+
servicenowSafety: loadServiceNowSafety()
|
|
45
|
+
},
|
|
46
|
+
missing: { clickup: clickupMissing, google: googleMissing }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function collectClickupMissing() {
|
|
50
|
+
var missing = [];
|
|
51
|
+
if (!process.env.CLICKUP_API_TOKEN)
|
|
52
|
+
missing.push("CLICKUP_API_TOKEN");
|
|
53
|
+
return missing;
|
|
54
|
+
}
|
|
55
|
+
function collectGoogleMissing() {
|
|
56
|
+
var missing = [];
|
|
57
|
+
if (!process.env.GOOGLE_CLIENT_ID)
|
|
58
|
+
missing.push("GOOGLE_CLIENT_ID");
|
|
59
|
+
if (!process.env.GOOGLE_CLIENT_SECRET)
|
|
60
|
+
missing.push("GOOGLE_CLIENT_SECRET");
|
|
61
|
+
if (!process.env.GOOGLE_REFRESH_TOKEN)
|
|
62
|
+
missing.push("GOOGLE_REFRESH_TOKEN");
|
|
63
|
+
return missing;
|
|
64
|
+
}
|
|
65
|
+
function loadServiceNowSafety() {
|
|
66
|
+
var deny = process.env.SINC_MCP_SN_TABLE_DENY
|
|
67
|
+
? splitCsv(process.env.SINC_MCP_SN_TABLE_DENY)
|
|
68
|
+
: DEFAULT_DENY_TABLES.slice();
|
|
69
|
+
var override = process.env.SINC_MCP_SN_TABLE_OVERRIDE
|
|
70
|
+
? splitCsv(process.env.SINC_MCP_SN_TABLE_OVERRIDE)
|
|
71
|
+
: [];
|
|
72
|
+
return { denyTables: deny, overrideTables: override };
|
|
73
|
+
}
|
|
74
|
+
function splitCsv(s) {
|
|
75
|
+
var raw = s.split(",");
|
|
76
|
+
var out = [];
|
|
77
|
+
for (var i = 0; i < raw.length; i++) {
|
|
78
|
+
var trimmed = raw[i].trim();
|
|
79
|
+
if (trimmed)
|
|
80
|
+
out.push(trimmed);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function formatMissingEnvError(missing) {
|
|
85
|
+
var parts = [];
|
|
86
|
+
if (missing.clickup.length > 0) {
|
|
87
|
+
parts.push("ClickUp: " + missing.clickup.join(", "));
|
|
88
|
+
}
|
|
89
|
+
if (missing.google.length > 0) {
|
|
90
|
+
parts.push("Google (Gmail/Calendar): " + missing.google.join(", "));
|
|
91
|
+
}
|
|
92
|
+
return "Missing required environment variables — " + parts.join("; ") + ".";
|
|
93
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps thrown errors from upstream Dovetail packages into MCP tool-result
|
|
3
|
+
* shapes. Upstream packages already produce remediation-friendly messages
|
|
4
|
+
* (e.g. handleAuthError in dovetail-google-auth) — preserve them verbatim.
|
|
5
|
+
*/
|
|
6
|
+
export interface ToolError {
|
|
7
|
+
message: string;
|
|
8
|
+
retryable: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function mapToolError(err: unknown): ToolError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Maps thrown errors from upstream Dovetail packages into MCP tool-result
|
|
4
|
+
* shapes. Upstream packages already produce remediation-friendly messages
|
|
5
|
+
* (e.g. handleAuthError in dovetail-google-auth) — preserve them verbatim.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.mapToolError = mapToolError;
|
|
9
|
+
function mapToolError(err) {
|
|
10
|
+
var message = err instanceof Error ? err.message : String(err);
|
|
11
|
+
var retryable = isRetryable(message);
|
|
12
|
+
return { message: message, retryable: retryable };
|
|
13
|
+
}
|
|
14
|
+
function isRetryable(message) {
|
|
15
|
+
var m = message.toLowerCase();
|
|
16
|
+
if (m.indexOf("rate limit") !== -1)
|
|
17
|
+
return true;
|
|
18
|
+
if (m.indexOf("429") !== -1)
|
|
19
|
+
return true;
|
|
20
|
+
if (m.indexOf("network") !== -1)
|
|
21
|
+
return true;
|
|
22
|
+
if (m.indexOf("etimedout") !== -1)
|
|
23
|
+
return true;
|
|
24
|
+
if (m.indexOf("econnreset") !== -1)
|
|
25
|
+
return true;
|
|
26
|
+
if (m.indexOf("retries exhausted") !== -1)
|
|
27
|
+
return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tenonhq/dovetail-mcp
|
|
3
|
+
*
|
|
4
|
+
* MCP server exposing read-only tools for ClickUp, Gmail, Google Calendar,
|
|
5
|
+
* and ServiceNow, backed by the existing Dovetail integration packages.
|
|
6
|
+
*
|
|
7
|
+
* Library exports here are consumed by tests and by alternate hosts
|
|
8
|
+
* (e.g. an HTTP transport in the future). The bin entry lives in server.ts.
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { ServiceNowSafetyConfig } from "./config";
|
|
12
|
+
import type { ClickUpConfig, GoogleConfig, SincMcpConfig } from "./config";
|
|
13
|
+
import { TOOL_NAMES } from "./registry";
|
|
14
|
+
import type { ToolName, RegistryDeps } from "./registry";
|
|
15
|
+
export { TOOL_NAMES };
|
|
16
|
+
export type { ToolName, SincMcpConfig, ClickUpConfig, GoogleConfig, ServiceNowSafetyConfig };
|
|
17
|
+
export interface CreateServerOptions {
|
|
18
|
+
/** Override loaded config; tests pass mocks here. */
|
|
19
|
+
registryDeps?: RegistryDeps;
|
|
20
|
+
/** Override server name shown to MCP clients. */
|
|
21
|
+
serverName?: string;
|
|
22
|
+
/** Override package version reported to MCP clients. */
|
|
23
|
+
serverVersion?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createServer(options?: CreateServerOptions): McpServer;
|
|
26
|
+
export declare function buildDepsFromEnv(): RegistryDeps;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @tenonhq/dovetail-mcp
|
|
4
|
+
*
|
|
5
|
+
* MCP server exposing read-only tools for ClickUp, Gmail, Google Calendar,
|
|
6
|
+
* and ServiceNow, backed by the existing Dovetail integration packages.
|
|
7
|
+
*
|
|
8
|
+
* Library exports here are consumed by tests and by alternate hosts
|
|
9
|
+
* (e.g. an HTTP transport in the future). The bin entry lives in server.ts.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TOOL_NAMES = void 0;
|
|
13
|
+
exports.createServer = createServer;
|
|
14
|
+
exports.buildDepsFromEnv = buildDepsFromEnv;
|
|
15
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16
|
+
const config_1 = require("./config");
|
|
17
|
+
const registry_1 = require("./registry");
|
|
18
|
+
Object.defineProperty(exports, "TOOL_NAMES", { enumerable: true, get: function () { return registry_1.TOOL_NAMES; } });
|
|
19
|
+
function createServer(options = {}) {
|
|
20
|
+
var server = new mcp_js_1.McpServer({
|
|
21
|
+
name: options.serverName || "dovetail-mcp",
|
|
22
|
+
version: options.serverVersion || "0.0.1"
|
|
23
|
+
});
|
|
24
|
+
var deps = options.registryDeps || buildDepsFromEnv();
|
|
25
|
+
(0, registry_1.registerAllTools)(server, deps);
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
function buildDepsFromEnv() {
|
|
29
|
+
var loaded = (0, config_1.loadConfig)();
|
|
30
|
+
var missingDescription;
|
|
31
|
+
var hasMissing = loaded.missing.clickup.length > 0 || loaded.missing.google.length > 0;
|
|
32
|
+
if (hasMissing) {
|
|
33
|
+
missingDescription = (0, config_1.formatMissingEnvError)(loaded.missing);
|
|
34
|
+
}
|
|
35
|
+
var deps = {
|
|
36
|
+
servicenow: { safety: loaded.config.servicenowSafety },
|
|
37
|
+
missingDescription: missingDescription
|
|
38
|
+
};
|
|
39
|
+
if (loaded.config.clickup) {
|
|
40
|
+
deps.clickup = { config: loaded.config.clickup };
|
|
41
|
+
}
|
|
42
|
+
if (loaded.config.google) {
|
|
43
|
+
deps.gmail = { config: loaded.config.google };
|
|
44
|
+
deps.calendar = { config: loaded.config.google };
|
|
45
|
+
}
|
|
46
|
+
return deps;
|
|
47
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument redaction for telemetry events.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only — testable without filesystem or env access. The goal is
|
|
5
|
+
* to keep operational signal (tool shape, query strings, IDs) while removing
|
|
6
|
+
* PII (email bodies, full email addresses) and credentials (tokens, passwords).
|
|
7
|
+
*/
|
|
8
|
+
export declare function redactArgs(args: unknown): unknown;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Argument redaction for telemetry events.
|
|
4
|
+
*
|
|
5
|
+
* Pure functions only — testable without filesystem or env access. The goal is
|
|
6
|
+
* to keep operational signal (tool shape, query strings, IDs) while removing
|
|
7
|
+
* PII (email bodies, full email addresses) and credentials (tokens, passwords).
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.redactArgs = redactArgs;
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
45
|
+
var TOKEN_KEYS = [
|
|
46
|
+
"token",
|
|
47
|
+
"password",
|
|
48
|
+
"refresh_token",
|
|
49
|
+
"refreshtoken",
|
|
50
|
+
"access_token",
|
|
51
|
+
"accesstoken",
|
|
52
|
+
"client_secret",
|
|
53
|
+
"clientsecret",
|
|
54
|
+
"apikey",
|
|
55
|
+
"api_key",
|
|
56
|
+
"clickup_api_token",
|
|
57
|
+
"authorization",
|
|
58
|
+
"auth"
|
|
59
|
+
];
|
|
60
|
+
var BODY_KEYS = ["body", "html", "text", "content"];
|
|
61
|
+
var QUERY_KEYS = [
|
|
62
|
+
"query",
|
|
63
|
+
"q",
|
|
64
|
+
"sysparm_query",
|
|
65
|
+
"subjectpatterns",
|
|
66
|
+
"labels",
|
|
67
|
+
"statuses"
|
|
68
|
+
];
|
|
69
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
70
|
+
function redactArgs(args) {
|
|
71
|
+
return walk(args, "");
|
|
72
|
+
}
|
|
73
|
+
function walk(value, keyPath) {
|
|
74
|
+
if (value === null || value === undefined) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
if (typeof value === "string") {
|
|
78
|
+
return redactString(value, keyPath);
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
var out = [];
|
|
85
|
+
for (var i = 0; i < value.length; i++) {
|
|
86
|
+
out.push(walk(value[i], keyPath));
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === "object") {
|
|
91
|
+
var record = value;
|
|
92
|
+
var result = {};
|
|
93
|
+
var keys = Object.keys(record);
|
|
94
|
+
for (var k = 0; k < keys.length; k++) {
|
|
95
|
+
var key = keys[k];
|
|
96
|
+
var lowered = key.toLowerCase();
|
|
97
|
+
if (TOKEN_KEYS.indexOf(lowered) !== -1) {
|
|
98
|
+
result[key] = "[REDACTED]";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (BODY_KEYS.indexOf(lowered) !== -1 && typeof record[key] === "string") {
|
|
102
|
+
result[key] = "[REDACTED:body]";
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
result[key] = walk(record[key], lowered);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function redactString(value, keyPath) {
|
|
112
|
+
if (QUERY_KEYS.indexOf(keyPath) !== -1) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
if (EMAIL_RE.test(value)) {
|
|
116
|
+
return maskEmail(value);
|
|
117
|
+
}
|
|
118
|
+
if (value.length > 200) {
|
|
119
|
+
return "sha256:" + sha256First12(value);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function maskEmail(addr) {
|
|
124
|
+
var at = addr.indexOf("@");
|
|
125
|
+
if (at < 0) {
|
|
126
|
+
return addr;
|
|
127
|
+
}
|
|
128
|
+
var local = addr.substring(0, at);
|
|
129
|
+
var domain = addr.substring(at + 1);
|
|
130
|
+
var prefix = local.length <= 3 ? local : local.substring(0, 3);
|
|
131
|
+
return prefix + "***@" + domain;
|
|
132
|
+
}
|
|
133
|
+
function sha256First12(s) {
|
|
134
|
+
return crypto.createHash("sha256").update(s, "utf8").digest("hex").substring(0, 12);
|
|
135
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registration glue. Exports TOOL_NAMES (the canonical 12-tuple) and
|
|
3
|
+
* registerAllTools(), which wires every handler with telemetry + JSON
|
|
4
|
+
* serialization for MCP transport.
|
|
5
|
+
*
|
|
6
|
+
* Dependencies are passed in by the caller — server.ts builds them from
|
|
7
|
+
* loadConfig(), tests inject mocks. This split keeps registry.ts pure of
|
|
8
|
+
* env access.
|
|
9
|
+
*/
|
|
10
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { ClickUpDeps } from "./tools/clickup";
|
|
13
|
+
import type { GmailDeps } from "./tools/gmail";
|
|
14
|
+
import type { CalendarDeps } from "./tools/calendar";
|
|
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"];
|
|
17
|
+
export type ToolName = typeof TOOL_NAMES[number];
|
|
18
|
+
export interface RegistryDeps {
|
|
19
|
+
clickup?: ClickUpDeps;
|
|
20
|
+
gmail?: GmailDeps;
|
|
21
|
+
calendar?: CalendarDeps;
|
|
22
|
+
servicenow: ServiceNowDeps;
|
|
23
|
+
missingDescription?: string;
|
|
24
|
+
}
|
|
25
|
+
interface ToolDescriptor {
|
|
26
|
+
name: ToolName;
|
|
27
|
+
description: string;
|
|
28
|
+
shape: z.ZodRawShape;
|
|
29
|
+
handler: (args: any) => Promise<any>;
|
|
30
|
+
}
|
|
31
|
+
export declare function registerAllTools(server: McpServer, deps: RegistryDeps): void;
|
|
32
|
+
export declare function buildDescriptorsForTests(deps: RegistryDeps): ToolDescriptor[];
|
|
33
|
+
export {};
|