@doist/todoist-mcp 10.3.2 → 10.3.3
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/dist/http-app.d.ts +19 -0
- package/dist/http-app.d.ts.map +1 -0
- package/dist/index.js +3 -3
- package/dist/main-http.js +113 -40
- package/dist/main.js +1 -1
- package/dist/{mcp-server-C_UexOyp.js → mcp-server-BiypwfCF.js} +1 -1
- package/dist/middleware/require-trusted-host.d.ts +44 -0
- package/dist/middleware/require-trusted-host.d.ts.map +1 -0
- package/dist/{require-valid-todoist-token-B0fNp1hu.js → require-valid-todoist-token-CMvIva7A.js} +1 -1
- package/dist/utils/host.d.ts +6 -0
- package/dist/utils/host.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
type CreateHttpAppOptions = {
|
|
3
|
+
todoistApiKey: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
/** Hostnames trusted in the Host/Origin headers (DNS-rebinding protection). */
|
|
6
|
+
allowedHosts: string[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build the Express app for the MCP HTTP server.
|
|
10
|
+
*
|
|
11
|
+
* The DNS-rebinding guard (`requireTrustedHost`) is scoped to the sensitive
|
|
12
|
+
* `/mcp` routes and runs ahead of body parsing and token auth there. `/health`
|
|
13
|
+
* is intentionally unguarded so deployment health probes — which send the
|
|
14
|
+
* target's private IP in the Host header — keep working; it exposes no account
|
|
15
|
+
* data.
|
|
16
|
+
*/
|
|
17
|
+
declare function createHttpApp({ todoistApiKey, baseUrl, allowedHosts }: CreateHttpAppOptions): Express;
|
|
18
|
+
export { createHttpApp, type CreateHttpAppOptions };
|
|
19
|
+
//# sourceMappingURL=http-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-app.d.ts","sourceRoot":"","sources":["../src/http-app.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,KAAK,OAAO,EAA+B,MAAM,SAAS,CAAA;AAK5E,KAAK,oBAAoB,GAAG;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED;;;;;;;;GAQG;AACH,iBAAS,aAAa,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAgD9F;AAED,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { f as s, s as a, l as e, m as t, a as o, u as d, r as i, d as r, b as n, c as l, e as c, h as p, i as m, j as f, k, n as T, o as u, p as g, q as j, t as v, v as P, w as h, x as S, y as b, z as A, A as y, B as C, C as G, D as F, E as x, F as E, G as L, H as O, I as w, J as H, K as I, L as M, M as q, N as z, O as B, P as D, Q as N, R, S as W } from "./mcp-server-
|
|
2
|
-
import { T as V, g as _ } from "./mcp-server-
|
|
3
|
-
import { r as Y, v as Z } from "./require-valid-todoist-token-
|
|
1
|
+
import { f as s, s as a, l as e, m as t, a as o, u as d, r as i, d as r, b as n, c as l, e as c, h as p, i as m, j as f, k, n as T, o as u, p as g, q as j, t as v, v as P, w as h, x as S, y as b, z as A, A as y, B as C, C as G, D as F, E as x, F as E, G as L, H as O, I as w, J as H, K as I, L as M, M as q, N as z, O as B, P as D, Q as N, R, S as W } from "./mcp-server-BiypwfCF.js";
|
|
2
|
+
import { T as V, g as _ } from "./mcp-server-BiypwfCF.js";
|
|
3
|
+
import { r as Y, v as Z } from "./require-valid-todoist-token-CMvIva7A.js";
|
|
4
4
|
const K = {
|
|
5
5
|
// Task management tools
|
|
6
6
|
addTasks: W,
|
package/dist/main-http.js
CHANGED
|
@@ -1,41 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { isIP as
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { g as
|
|
7
|
-
import { r as
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
2
|
+
import { isIP as p } from "node:net";
|
|
3
|
+
import O from "dotenv";
|
|
4
|
+
import { StreamableHTTPServerTransport as S } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import m from "express";
|
|
6
|
+
import { g as v } from "./mcp-server-BiypwfCF.js";
|
|
7
|
+
import { r as w } from "./require-valid-todoist-token-CMvIva7A.js";
|
|
8
|
+
function f(t) {
|
|
9
|
+
return t.startsWith("[") && t.endsWith("]") ? t.slice(1, -1) : t;
|
|
10
|
+
}
|
|
11
|
+
function I(t) {
|
|
12
|
+
return t = f(t), t === "localhost" || t === "::1" || t === "0:0:0:0:0:0:0:1" ? !0 : p(t) === 4 ? t.split(".")[0] === "127" : !1;
|
|
13
|
+
}
|
|
14
|
+
const _ = ["localhost", "127.0.0.1", "[::1]"], g = /[@/\\?#\s]/;
|
|
15
|
+
function h(t) {
|
|
16
|
+
if (!g.test(t))
|
|
17
|
+
try {
|
|
18
|
+
return new URL(`http://${t}`).hostname.toLowerCase();
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function L(t) {
|
|
24
|
+
let r;
|
|
25
|
+
try {
|
|
26
|
+
r = new URL(t);
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!(r.username || r.password || r.pathname !== "/" || r.search || r.hash))
|
|
31
|
+
return r.hostname.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
function A(t, r) {
|
|
34
|
+
const o = new Set(_);
|
|
35
|
+
for (const s of (r ?? "").split(",")) {
|
|
36
|
+
const n = s.trim();
|
|
37
|
+
n && o.add(n);
|
|
38
|
+
}
|
|
39
|
+
const e = f(t);
|
|
40
|
+
return e && e !== "0.0.0.0" && e !== "::" && o.add(p(e) === 6 ? `[${e}]` : e), [...o];
|
|
41
|
+
}
|
|
42
|
+
function d(t, r) {
|
|
43
|
+
t.status(403).json({
|
|
44
|
+
jsonrpc: "2.0",
|
|
45
|
+
error: { code: -32e3, message: r },
|
|
46
|
+
id: null
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function b(t) {
|
|
50
|
+
const r = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const o of t.allowedHosts) {
|
|
52
|
+
const e = h(o);
|
|
53
|
+
if (!e)
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid allowed host ${JSON.stringify(o)}: must be a hostname or host:port (IPv6 literals must be bracketed, e.g. [::1])`
|
|
56
|
+
);
|
|
57
|
+
r.add(e);
|
|
58
|
+
}
|
|
59
|
+
return (o, e, s) => {
|
|
60
|
+
const n = o.headers.host;
|
|
61
|
+
if (!n) {
|
|
62
|
+
d(e, "Missing Host header");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const i = h(n);
|
|
66
|
+
if (!i || !r.has(i)) {
|
|
67
|
+
d(e, `Invalid Host: ${n}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const a = o.headers.origin;
|
|
71
|
+
if (a) {
|
|
72
|
+
const l = L(a);
|
|
73
|
+
if (!l || !r.has(l)) {
|
|
74
|
+
d(e, `Invalid Origin: ${a}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
s();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function E({ todoistApiKey: t, baseUrl: r, allowedHosts: o }) {
|
|
82
|
+
const e = m(), s = b({ allowedHosts: o });
|
|
83
|
+
return e.get("/health", (n, i) => {
|
|
84
|
+
i.json({ status: "ok" });
|
|
85
|
+
}), e.post(
|
|
29
86
|
"/mcp",
|
|
30
|
-
|
|
31
|
-
|
|
87
|
+
s,
|
|
88
|
+
m.json(),
|
|
89
|
+
w({ type: "static", apiKey: t, baseUrl: r }),
|
|
90
|
+
async (n, i) => {
|
|
32
91
|
try {
|
|
33
|
-
const
|
|
92
|
+
const a = new S({
|
|
34
93
|
sessionIdGenerator: void 0
|
|
35
94
|
});
|
|
36
|
-
await
|
|
37
|
-
} catch (
|
|
38
|
-
console.error("[Error] Request handling failed:",
|
|
95
|
+
await v({ todoistApiKey: t, baseUrl: r }).connect(a), await a.handleRequest(n, i, n.body);
|
|
96
|
+
} catch (a) {
|
|
97
|
+
console.error("[Error] Request handling failed:", a), i.status(500).json({
|
|
39
98
|
jsonrpc: "2.0",
|
|
40
99
|
error: {
|
|
41
100
|
code: -32603,
|
|
@@ -45,13 +104,27 @@ function I() {
|
|
|
45
104
|
});
|
|
46
105
|
}
|
|
47
106
|
}
|
|
48
|
-
),
|
|
49
|
-
|
|
50
|
-
}),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
), e.get("/mcp", s, (n, i) => {
|
|
108
|
+
i.status(405).set("Allow", "POST").send("Method Not Allowed");
|
|
109
|
+
}), e;
|
|
110
|
+
}
|
|
111
|
+
O.config({ quiet: !0 });
|
|
112
|
+
const c = Number.parseInt(process.env.PORT || "3000", 10), T = process.env.HOST || "127.0.0.1", u = f(T);
|
|
113
|
+
function P(t) {
|
|
114
|
+
return t === "0.0.0.0" || t === "::" ? "localhost" : H(t);
|
|
115
|
+
}
|
|
116
|
+
function H(t) {
|
|
117
|
+
return p(t) === 6 ? `[${t}]` : t;
|
|
118
|
+
}
|
|
119
|
+
function $() {
|
|
120
|
+
const t = process.env.TODOIST_BASE_URL, r = process.env.TODOIST_API_KEY;
|
|
121
|
+
r || (console.error("Error: TODOIST_API_KEY environment variable is required"), process.exit(1));
|
|
122
|
+
const o = A(T, process.env.ALLOWED_HOSTS);
|
|
123
|
+
E({ todoistApiKey: r, baseUrl: t, allowedHosts: o }).listen(c, u, () => {
|
|
124
|
+
const s = P(u);
|
|
125
|
+
console.error(`Todoist MCP HTTP Server started on ${H(u)}:${c}`), console.error(`MCP endpoint: http://${s}:${c}/mcp`), console.error(`Health check: http://${s}:${c}/health`), I(u) || console.error(
|
|
126
|
+
"Warning: todoist-mcp-http is reachable from other hosts. Set ALLOWED_HOSTS to the hostnames clients use and protect this service with trusted network/auth controls because requests run with TODOIST_API_KEY."
|
|
54
127
|
);
|
|
55
128
|
});
|
|
56
129
|
}
|
|
57
|
-
|
|
130
|
+
$();
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport as s } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import c from "dotenv";
|
|
4
|
-
import { g as i } from "./mcp-server-
|
|
4
|
+
import { g as i } from "./mcp-server-BiypwfCF.js";
|
|
5
5
|
function p() {
|
|
6
6
|
const e = process.env.TODOIST_BASE_URL, r = process.env.TODOIST_API_KEY;
|
|
7
7
|
if (!r)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
type RequireTrustedHostOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Port-agnostic hostnames permitted in the `Host` and `Origin` headers.
|
|
5
|
+
* IPv6 entries must be bracketed, e.g. `[::1]`. Invalid entries cause
|
|
6
|
+
* {@link requireTrustedHost} to throw at construction time (fail fast).
|
|
7
|
+
*/
|
|
8
|
+
allowedHosts: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Loopback hostnames always trusted by {@link requireTrustedHost}. Stored
|
|
12
|
+
* bracketed for IPv6 because `new URL('http://[::1]').hostname` returns `[::1]`.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_ALLOWED_HOSTS: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Build the list of hostnames trusted in the Host and Origin headers. Always
|
|
17
|
+
* includes the loopback defaults, plus any names from `allowedHostsEnv`
|
|
18
|
+
* (comma-separated), plus the configured `host` (so an explicit bind — loopback
|
|
19
|
+
* alias like 127.0.1.1, a LAN IP, etc. — is always trusted). Bind wildcards
|
|
20
|
+
* (0.0.0.0/::) are never added because they are never valid Host header values —
|
|
21
|
+
* operators exposing on a wildcard must enumerate client hostnames via
|
|
22
|
+
* ALLOWED_HOSTS.
|
|
23
|
+
*/
|
|
24
|
+
declare function buildAllowedHosts(host: string, allowedHostsEnv?: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Express middleware that rejects requests whose `Host` or `Origin` header is
|
|
27
|
+
* not in the trusted allowlist. This is DNS-rebinding protection: a malicious
|
|
28
|
+
* website that rebinds its domain to a victim's loopback address keeps its own
|
|
29
|
+
* `Host`/`Origin`, so string-matching against the allowlist (we never DNS
|
|
30
|
+
* resolve) blocks the request before it can reach the MCP transport.
|
|
31
|
+
*
|
|
32
|
+
* - Missing `Host` → 403.
|
|
33
|
+
* - `Host` hostname not in the allowlist → 403.
|
|
34
|
+
* - `Origin` absent → allowed (non-browser clients such as `mcp-remote`/`curl`
|
|
35
|
+
* send none; the browser attack path always sends one).
|
|
36
|
+
* - `Origin` present but untrusted or unparseable → 403.
|
|
37
|
+
*
|
|
38
|
+
* Throws at construction time if `allowedHosts` contains an entry that isn't a
|
|
39
|
+
* valid authority, so misconfiguration fails fast instead of becoming opaque
|
|
40
|
+
* 403s at request time.
|
|
41
|
+
*/
|
|
42
|
+
declare function requireTrustedHost(options: RequireTrustedHostOptions): RequestHandler;
|
|
43
|
+
export { DEFAULT_ALLOWED_HOSTS, buildAllowedHosts, requireTrustedHost, type RequireTrustedHostOptions, };
|
|
44
|
+
//# sourceMappingURL=require-trusted-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-trusted-host.d.ts","sourceRoot":"","sources":["../../src/middleware/require-trusted-host.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAA;AAG9E,KAAK,yBAAyB,GAAG;IAC7B;;;;OAIG;IACH,YAAY,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED;;;GAGG;AACH,QAAA,MAAM,qBAAqB,UAAsC,CAAA;AA4CjE;;;;;;;;GAQG;AACH,iBAAS,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAkB3E;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,cAAc,CAyC9E;AAED,OAAO,EACH,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,yBAAyB,GACjC,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Strip the surrounding brackets from a bracketed IPv6 literal, if present. */
|
|
2
|
+
declare function normalizeListenHost(host: string): string;
|
|
3
|
+
/** Whether `host` refers to a loopback interface (localhost / 127.0.0.0/8 / ::1). */
|
|
4
|
+
declare function isLoopbackHost(host: string): boolean;
|
|
5
|
+
export { isLoopbackHost, normalizeListenHost };
|
|
6
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/utils/host.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,iBAAS,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,qFAAqF;AACrF,iBAAS,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAY7C;AAED,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAA"}
|