@thingd/cli 0.31.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 (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/dist/dashboard/public/assets/index-B-Y-3-0l.js +2 -0
  4. package/dist/dashboard/public/assets/index-B5YhpIl3.js +2 -0
  5. package/dist/dashboard/public/assets/index-BnFclxvN.css +1 -0
  6. package/dist/dashboard/public/assets/index-BtA9rnyI.js +2 -0
  7. package/dist/dashboard/public/assets/index-BzLTzidY.js +2 -0
  8. package/dist/dashboard/public/assets/index-C6PkDB7y.css +1 -0
  9. package/dist/dashboard/public/assets/index-D8yUCdOQ.js +2 -0
  10. package/dist/dashboard/public/assets/index-fQywB2df.js +2 -0
  11. package/dist/dashboard/public/assets/index-kZdrdi3K.css +1 -0
  12. package/dist/dashboard/public/assets/index-kgZrboBN.js +4 -0
  13. package/dist/dashboard/public/favicon.svg +1 -0
  14. package/dist/dashboard/public/icons.svg +24 -0
  15. package/dist/dashboard/public/index.html +16 -0
  16. package/dist/dashboard/server.d.ts +6 -0
  17. package/dist/dashboard/server.d.ts.map +1 -0
  18. package/dist/dashboard/server.js +385 -0
  19. package/dist/data-movement.d.ts +5 -0
  20. package/dist/data-movement.d.ts.map +1 -0
  21. package/dist/data-movement.js +257 -0
  22. package/dist/doctor.d.ts +3 -0
  23. package/dist/doctor.d.ts.map +1 -0
  24. package/dist/doctor.js +109 -0
  25. package/dist/index.d.ts +42 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1015 -0
  28. package/dist/install.d.ts +3 -0
  29. package/dist/install.d.ts.map +1 -0
  30. package/dist/install.js +311 -0
  31. package/dist/interactive.d.ts +2 -0
  32. package/dist/interactive.d.ts.map +1 -0
  33. package/dist/interactive.js +1592 -0
  34. package/dist/logo.d.ts +3 -0
  35. package/dist/logo.d.ts.map +1 -0
  36. package/dist/logo.js +8 -0
  37. package/dist/mcp/audit.d.ts +27 -0
  38. package/dist/mcp/audit.d.ts.map +1 -0
  39. package/dist/mcp/audit.js +36 -0
  40. package/dist/mcp/cluster.d.ts +68 -0
  41. package/dist/mcp/cluster.d.ts.map +1 -0
  42. package/dist/mcp/cluster.js +303 -0
  43. package/dist/mcp/config.d.ts +14 -0
  44. package/dist/mcp/config.d.ts.map +1 -0
  45. package/dist/mcp/config.js +67 -0
  46. package/dist/mcp/http.d.ts +25 -0
  47. package/dist/mcp/http.d.ts.map +1 -0
  48. package/dist/mcp/http.js +588 -0
  49. package/dist/mcp/index.d.ts +5 -0
  50. package/dist/mcp/index.d.ts.map +1 -0
  51. package/dist/mcp/index.js +3 -0
  52. package/dist/mcp/result.d.ts +3 -0
  53. package/dist/mcp/result.d.ts.map +1 -0
  54. package/dist/mcp/result.js +10 -0
  55. package/dist/mcp/server.d.ts +19 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +51 -0
  58. package/dist/mcp/tools.d.ts +10 -0
  59. package/dist/mcp/tools.d.ts.map +1 -0
  60. package/dist/mcp/tools.js +568 -0
  61. package/dist/mcp-http.d.ts +3 -0
  62. package/dist/mcp-http.d.ts.map +1 -0
  63. package/dist/mcp-http.js +42 -0
  64. package/dist/mcp.d.ts +3 -0
  65. package/dist/mcp.d.ts.map +1 -0
  66. package/dist/mcp.js +22 -0
  67. package/dist/paths.d.ts +4 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +14 -0
  70. package/dist/rest/helpers.d.ts +17 -0
  71. package/dist/rest/helpers.d.ts.map +1 -0
  72. package/dist/rest/helpers.js +55 -0
  73. package/dist/rest/server.d.ts +4 -0
  74. package/dist/rest/server.d.ts.map +1 -0
  75. package/dist/rest/server.js +317 -0
  76. package/package.json +57 -0
package/dist/logo.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function logoText(): string;
2
+ export declare function logoLine(): string;
3
+ //# sourceMappingURL=logo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logo.d.ts","sourceRoot":"","sources":["../src/logo.ts"],"names":[],"mappings":"AAGA,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
package/dist/logo.js ADDED
@@ -0,0 +1,8 @@
1
+ const orange = (s) => `\x1b[38;2;224;83;22m${s}\x1b[0m`;
2
+ const cyan = (s) => `\x1b[38;2;0;196;212m${s}\x1b[0m`;
3
+ export function logoText() {
4
+ return `${orange("{")}${cyan("t")}${cyan("h")}${cyan("i")}${cyan("n")}${cyan("g")}${orange(":")}${orange("d")}${orange("}")}`;
5
+ }
6
+ export function logoLine() {
7
+ return `${logoText()}\n`;
8
+ }
@@ -0,0 +1,27 @@
1
+ import type { ThingD } from "thingd";
2
+ export type ThingdMcpAuditOptions = {
3
+ enabled?: boolean;
4
+ actor?: string;
5
+ source?: string;
6
+ stream?: string;
7
+ };
8
+ export type ThingdMcpAuditMetadata = {
9
+ actor?: string;
10
+ source?: string;
11
+ };
12
+ type ResolvedThingdMcpAuditOptions = {
13
+ enabled: boolean;
14
+ actor: string;
15
+ source: string;
16
+ stream: string;
17
+ };
18
+ type ThingdMcpAuditEventOptions = {
19
+ action: string;
20
+ target: Record<string, unknown>;
21
+ metadata?: ThingdMcpAuditMetadata;
22
+ result?: Record<string, unknown>;
23
+ };
24
+ export declare function resolveThingdMcpAuditOptions(options: ThingdMcpAuditOptions | false | undefined): ResolvedThingdMcpAuditOptions;
25
+ export declare function appendMcpAuditEvent(db: ThingD, options: ResolvedThingdMcpAuditOptions, event: ThingdMcpAuditEventOptions): Promise<void>;
26
+ export {};
27
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/mcp/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,QAAQ,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC;AAMF,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,qBAAqB,GAAG,KAAK,GAAG,SAAS,GACjD,6BAA6B,CAgB/B;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,6BAA6B,EACtC,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -0,0 +1,36 @@
1
+ const DEFAULT_AUDIT_STREAM = "__thingd:mcp:audit";
2
+ const DEFAULT_AUDIT_ACTOR = "mcp-client";
3
+ const DEFAULT_AUDIT_SOURCE = "thingd-mcp";
4
+ export function resolveThingdMcpAuditOptions(options) {
5
+ if (options === false || options?.enabled === false) {
6
+ return {
7
+ enabled: false,
8
+ actor: DEFAULT_AUDIT_ACTOR,
9
+ source: DEFAULT_AUDIT_SOURCE,
10
+ stream: DEFAULT_AUDIT_STREAM,
11
+ };
12
+ }
13
+ return {
14
+ enabled: true,
15
+ actor: options?.actor ?? DEFAULT_AUDIT_ACTOR,
16
+ source: options?.source ?? DEFAULT_AUDIT_SOURCE,
17
+ stream: options?.stream ?? DEFAULT_AUDIT_STREAM,
18
+ };
19
+ }
20
+ export async function appendMcpAuditEvent(db, options, event) {
21
+ if (!options.enabled) {
22
+ return;
23
+ }
24
+ const actor = event.metadata?.actor ?? options.actor;
25
+ const source = event.metadata?.source ?? options.source;
26
+ const auditEvent = {
27
+ type: `mcp.${event.action}`,
28
+ text: `MCP ${event.action} by ${actor}`,
29
+ actor,
30
+ source,
31
+ target: event.target,
32
+ result: event.result,
33
+ at: new Date().toISOString(),
34
+ };
35
+ await db.events.append(options.stream, auditEvent);
36
+ }
@@ -0,0 +1,68 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { ThingD } from "@thingd/node";
3
+ export type ThingdClusterMode = "single" | "leader" | "follower";
4
+ export type ThingdClusterDiscovery = "none" | "static" | "kubernetes";
5
+ export type ThingdClusterOptions = {
6
+ mode?: ThingdClusterMode;
7
+ advertiseUrl?: string;
8
+ leaderUrl?: string;
9
+ fallbackLeaderUrl?: string;
10
+ peers?: string[];
11
+ discovery?: ThingdClusterDiscovery;
12
+ service?: string;
13
+ namespace?: string;
14
+ port?: number;
15
+ forwardAuthToken?: string;
16
+ statusPath?: string;
17
+ peersPath?: string;
18
+ /** Enable automatic leader failover. Default: false. */
19
+ leaderElection?: boolean;
20
+ /** Consecutive replication failures before triggering election. Default: 3. */
21
+ electionMaxFailures?: number;
22
+ };
23
+ export type ResolvedThingdClusterOptions = {
24
+ mode: ThingdClusterMode;
25
+ advertiseUrl?: string;
26
+ leaderUrl?: string;
27
+ fallbackLeaderUrl?: string;
28
+ activeLeaderUrl?: string;
29
+ peers: string[];
30
+ discovery: ThingdClusterDiscovery;
31
+ service?: string;
32
+ namespace?: string;
33
+ port: number;
34
+ forwardAuthToken?: string;
35
+ statusPath: string;
36
+ peersPath: string;
37
+ leaderElection: boolean;
38
+ electionMaxFailures: number;
39
+ /** Cached replication lag (events behind leader). Updated by the replication runner. */
40
+ cachedReplicationLag: number;
41
+ };
42
+ export type ThingdClusterStatus = {
43
+ mode: ThingdClusterMode;
44
+ writable: boolean;
45
+ forwarding: boolean;
46
+ leaderUrl?: string;
47
+ fallbackLeaderUrl?: string;
48
+ activeLeaderUrl?: string;
49
+ advertiseUrl?: string;
50
+ discovery: ThingdClusterDiscovery;
51
+ peers: string[];
52
+ leaderElection: boolean;
53
+ electionMaxFailures: number;
54
+ replication: {
55
+ lastReplicatedSequence: number;
56
+ status: string;
57
+ lag?: number;
58
+ } | "not-implemented";
59
+ };
60
+ export declare function readClusterOptionsFromEnv(env: Record<string, string | undefined>): ThingdClusterOptions;
61
+ export declare function resolveClusterOptions(options: ThingdClusterOptions | undefined): ResolvedThingdClusterOptions;
62
+ export declare function getClusterStatus(cluster: ResolvedThingdClusterOptions, db: ThingD): Promise<ThingdClusterStatus>;
63
+ export declare function forwardMcpRequestToLeader(cluster: ResolvedThingdClusterOptions, mcpPath: string, request: IncomingMessage, response: ServerResponse): Promise<void>;
64
+ export declare function findNextLeaderCandidate(cluster: ResolvedThingdClusterOptions): {
65
+ url: string;
66
+ isSelf: boolean;
67
+ } | undefined;
68
+ //# sourceMappingURL=cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/mcp/cluster.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,303 @@
1
+ import { parseBooleanFlag, parsePort } from "./config.js";
2
+ const DEFAULT_CLUSTER_PORT = 8757;
3
+ export function readClusterOptionsFromEnv(env) {
4
+ return {
5
+ mode: parseClusterMode(env.THINGD_CLUSTER_MODE),
6
+ advertiseUrl: env.THINGD_ADVERTISE_URL,
7
+ leaderUrl: env.THINGD_CLUSTER_LEADER_URL,
8
+ fallbackLeaderUrl: env.THINGD_CLUSTER_LEADER_FALLBACK_URL,
9
+ peers: parsePeers(env.THINGD_CLUSTER_PEERS),
10
+ discovery: parseClusterDiscovery(env.THINGD_CLUSTER_DISCOVERY),
11
+ service: env.THINGD_CLUSTER_SERVICE,
12
+ namespace: env.THINGD_CLUSTER_NAMESPACE,
13
+ port: parsePort(env.THINGD_CLUSTER_PORT, DEFAULT_CLUSTER_PORT),
14
+ forwardAuthToken: env.THINGD_CLUSTER_FORWARD_AUTH_TOKEN ?? env.THINGD_AUTH_TOKEN,
15
+ leaderElection: parseBooleanFlag(env.THINGD_CLUSTER_LEADER_ELECTION, "THINGD_CLUSTER_LEADER_ELECTION"),
16
+ electionMaxFailures: parsePositiveInt(env.THINGD_CLUSTER_LEADER_ELECTION_MAX_FAILURES, 3),
17
+ };
18
+ }
19
+ export function resolveClusterOptions(options) {
20
+ const mode = options?.mode ?? "single";
21
+ const discovery = options?.discovery ?? (options?.peers?.length ? "static" : "none");
22
+ const port = options?.port ?? DEFAULT_CLUSTER_PORT;
23
+ const peers = resolvePeers({
24
+ discovery,
25
+ peers: options?.peers ?? [],
26
+ service: options?.service,
27
+ namespace: options?.namespace,
28
+ port,
29
+ });
30
+ const leaderElection = options?.leaderElection ?? false;
31
+ const electionMaxFailures = options?.electionMaxFailures ?? 3;
32
+ // When leader election is enabled, derive leaderUrl from peer list if not explicitly set.
33
+ if (mode === "follower" && !options?.leaderUrl && leaderElection && peers.length > 0) {
34
+ options = { ...options, leaderUrl: peers[0] };
35
+ }
36
+ if (mode === "follower" && !options?.leaderUrl) {
37
+ throw new Error("THINGD_CLUSTER_LEADER_URL is required when THINGD_CLUSTER_MODE=follower");
38
+ }
39
+ return {
40
+ mode,
41
+ advertiseUrl: options?.advertiseUrl,
42
+ leaderUrl: options?.leaderUrl,
43
+ fallbackLeaderUrl: options?.fallbackLeaderUrl,
44
+ activeLeaderUrl: undefined,
45
+ peers,
46
+ discovery,
47
+ service: options?.service,
48
+ namespace: options?.namespace,
49
+ port,
50
+ forwardAuthToken: options?.forwardAuthToken,
51
+ statusPath: options?.statusPath ?? "/cluster/status",
52
+ peersPath: options?.peersPath ?? "/cluster/peers",
53
+ leaderElection,
54
+ electionMaxFailures,
55
+ cachedReplicationLag: 0,
56
+ };
57
+ }
58
+ export async function getClusterStatus(cluster, db) {
59
+ let replication = "not-implemented";
60
+ if (cluster.mode === "follower") {
61
+ try {
62
+ const status = await db.get("__thingd_meta", "replication_status");
63
+ const lastSeq = status && typeof status.lastReplicatedSequence === "number"
64
+ ? status.lastReplicatedSequence
65
+ : 0;
66
+ // Lag is updated asynchronously by the replication runner — read cached value
67
+ // to avoid a synchronous outbound HTTP call on every /healthz request.
68
+ replication = {
69
+ lastReplicatedSequence: lastSeq,
70
+ status: "syncing",
71
+ lag: cluster.cachedReplicationLag,
72
+ };
73
+ }
74
+ catch {
75
+ replication = { lastReplicatedSequence: 0, status: "error" };
76
+ }
77
+ }
78
+ else if (cluster.mode === "leader" || cluster.mode === "single") {
79
+ try {
80
+ const list = await db.events.list("__thingd:system:replication");
81
+ const lastEvent = list[list.length - 1];
82
+ const lastSeq = lastEvent ? Number.parseInt(lastEvent.id, 10) : 0;
83
+ replication = {
84
+ lastReplicatedSequence: lastSeq,
85
+ status: "active",
86
+ };
87
+ }
88
+ catch {
89
+ replication = { lastReplicatedSequence: 0, status: "error" };
90
+ }
91
+ }
92
+ return {
93
+ mode: cluster.mode,
94
+ writable: cluster.mode !== "follower",
95
+ forwarding: cluster.mode === "follower",
96
+ leaderUrl: cluster.leaderUrl,
97
+ fallbackLeaderUrl: cluster.fallbackLeaderUrl,
98
+ activeLeaderUrl: cluster.activeLeaderUrl,
99
+ advertiseUrl: cluster.advertiseUrl,
100
+ discovery: cluster.discovery,
101
+ peers: cluster.peers,
102
+ leaderElection: cluster.leaderElection,
103
+ electionMaxFailures: cluster.electionMaxFailures,
104
+ replication,
105
+ };
106
+ }
107
+ export async function forwardMcpRequestToLeader(cluster, mcpPath, request, response) {
108
+ if (!cluster.leaderUrl) {
109
+ writeForwardError(response, 503, "cluster_leader_unavailable");
110
+ return;
111
+ }
112
+ const body = await readRequestBody(request);
113
+ const urls = cluster.fallbackLeaderUrl
114
+ ? [cluster.leaderUrl, cluster.fallbackLeaderUrl]
115
+ : [cluster.leaderUrl];
116
+ let lastError;
117
+ for (const url of urls) {
118
+ try {
119
+ const upstreamUrl = leaderMcpUrl(url, mcpPath);
120
+ const upstream = await fetch(upstreamUrl, {
121
+ method: "POST",
122
+ headers: forwardedHeaders(request, cluster.forwardAuthToken),
123
+ body: new Uint8Array(body),
124
+ signal: AbortSignal.timeout(30_000),
125
+ });
126
+ cluster.activeLeaderUrl = url;
127
+ response.writeHead(upstream.status, responseHeaders(upstream.headers));
128
+ response.end(Buffer.from(await upstream.arrayBuffer()));
129
+ return;
130
+ }
131
+ catch (error) {
132
+ lastError = error instanceof Error ? error : new Error(String(error));
133
+ console.error(`Forward to leader ${url} failed:`, lastError.message);
134
+ }
135
+ }
136
+ if (lastError) {
137
+ console.error("All leader URLs exhausted for forward, last error:", lastError.message);
138
+ }
139
+ writeForwardError(response, 503, "cluster_leader_unavailable");
140
+ }
141
+ export function findNextLeaderCandidate(cluster) {
142
+ const { advertiseUrl, peers } = cluster;
143
+ const currentLeaderUrl = cluster.activeLeaderUrl ?? cluster.leaderUrl;
144
+ if (!currentLeaderUrl || peers.length === 0) {
145
+ return undefined;
146
+ }
147
+ const leaderIndex = findPeerIndex(peers, currentLeaderUrl);
148
+ if (leaderIndex === -1) {
149
+ return undefined;
150
+ }
151
+ // Scan from the next peer after the current leader.
152
+ for (let i = leaderIndex + 1; i < peers.length; i++) {
153
+ const candidate = peers[i];
154
+ if (candidate) {
155
+ const isSelf = typeof advertiseUrl === "string" && sameOrigin(advertiseUrl, candidate);
156
+ return { url: candidate, isSelf };
157
+ }
158
+ }
159
+ // Wrap around — all peers after leader's index exhausted, try from the beginning.
160
+ for (let i = 0; i < leaderIndex; i++) {
161
+ const candidate = peers[i];
162
+ if (candidate) {
163
+ const isSelf = typeof advertiseUrl === "string" && sameOrigin(advertiseUrl, candidate);
164
+ return { url: candidate, isSelf };
165
+ }
166
+ }
167
+ return undefined;
168
+ }
169
+ function findPeerIndex(peers, targetUrl) {
170
+ return peers.findIndex((peer) => sameOrigin(peer, targetUrl));
171
+ }
172
+ /** Compare two URLs by origin (scheme + host + port). */
173
+ function sameOrigin(a, b) {
174
+ try {
175
+ return new URL(a).origin === new URL(b).origin;
176
+ }
177
+ catch {
178
+ return a === b;
179
+ }
180
+ }
181
+ function parsePositiveInt(value, fallback) {
182
+ if (!value) {
183
+ return fallback;
184
+ }
185
+ const n = Number.parseInt(value, 10);
186
+ if (!Number.isInteger(n) || n <= 0) {
187
+ throw new Error(`Invalid positive integer: ${value}`);
188
+ }
189
+ return n;
190
+ }
191
+ function parseClusterMode(value) {
192
+ if (!value) {
193
+ return undefined;
194
+ }
195
+ if (value === "single" || value === "leader" || value === "follower") {
196
+ return value;
197
+ }
198
+ throw new Error(`Unsupported THINGD_CLUSTER_MODE: ${value}`);
199
+ }
200
+ function parseClusterDiscovery(value) {
201
+ if (!value) {
202
+ return undefined;
203
+ }
204
+ if (value === "none" || value === "static" || value === "kubernetes") {
205
+ return value;
206
+ }
207
+ throw new Error(`Unsupported THINGD_CLUSTER_DISCOVERY: ${value}`);
208
+ }
209
+ function parsePeers(value) {
210
+ if (!value) {
211
+ return undefined;
212
+ }
213
+ return value
214
+ .split(",")
215
+ .map((peer) => peer.trim())
216
+ .filter(Boolean);
217
+ }
218
+ function resolvePeers(options) {
219
+ if (options.discovery === "static") {
220
+ return options.peers;
221
+ }
222
+ if (options.discovery !== "kubernetes" || !options.service) {
223
+ return [];
224
+ }
225
+ const namespace = options.namespace ?? "default";
226
+ return [`http://${options.service}.${namespace}.svc.cluster.local:${options.port}`];
227
+ }
228
+ function leaderMcpUrl(leaderUrl, mcpPath) {
229
+ const url = new URL(leaderUrl);
230
+ if (url.pathname === "/" || url.pathname === "") {
231
+ url.pathname = mcpPath;
232
+ }
233
+ return url.toString();
234
+ }
235
+ function forwardedHeaders(request, forwardAuthToken) {
236
+ const headers = new Headers();
237
+ const contentType = request.headers["content-type"];
238
+ const protocolVersion = request.headers["mcp-protocol-version"];
239
+ const accept = request.headers.accept;
240
+ if (typeof contentType === "string") {
241
+ headers.set("Content-Type", contentType);
242
+ }
243
+ if (typeof accept === "string") {
244
+ headers.set("Accept", accept);
245
+ }
246
+ if (typeof protocolVersion === "string") {
247
+ headers.set("MCP-Protocol-Version", protocolVersion);
248
+ }
249
+ if (forwardAuthToken) {
250
+ headers.set("Authorization", `Bearer ${forwardAuthToken}`);
251
+ }
252
+ else if (typeof request.headers.authorization === "string") {
253
+ headers.set("Authorization", request.headers.authorization);
254
+ }
255
+ return headers;
256
+ }
257
+ function responseHeaders(headers) {
258
+ const result = {};
259
+ for (const [key, value] of headers) {
260
+ if (!isHopByHopHeader(key)) {
261
+ result[key] = value;
262
+ }
263
+ }
264
+ return result;
265
+ }
266
+ function isHopByHopHeader(header) {
267
+ return [
268
+ "connection",
269
+ "content-length",
270
+ "keep-alive",
271
+ "proxy-authenticate",
272
+ "proxy-authorization",
273
+ "te",
274
+ "trailer",
275
+ "transfer-encoding",
276
+ "upgrade",
277
+ ].includes(header.toLowerCase());
278
+ }
279
+ function readRequestBody(request) {
280
+ return new Promise((resolve, reject) => {
281
+ const chunks = [];
282
+ request.on("data", (chunk) => {
283
+ chunks.push(chunk);
284
+ });
285
+ request.on("end", () => {
286
+ resolve(Buffer.concat(chunks));
287
+ });
288
+ request.on("error", reject);
289
+ });
290
+ }
291
+ function writeForwardError(response, statusCode, error) {
292
+ response.writeHead(statusCode, {
293
+ "Content-Type": "application/json",
294
+ });
295
+ response.end(JSON.stringify({
296
+ jsonrpc: "2.0",
297
+ error: {
298
+ code: -32_603,
299
+ message: error,
300
+ },
301
+ id: null,
302
+ }));
303
+ }
@@ -0,0 +1,14 @@
1
+ import type { ThingDDriver, ThingdMcpAuditOptions } from "@thingd/node";
2
+ export type ThingDStorageDriver = Exclude<ThingDDriver, "remote">;
3
+ export type HttpRuntimeSafetyOptions = {
4
+ host: string;
5
+ authToken?: string;
6
+ allowUnauthenticated?: boolean;
7
+ };
8
+ export declare function parseThingdDriver(value: string | undefined): ThingDStorageDriver | undefined;
9
+ export declare function parsePort(value: string | undefined, fallback: number): number;
10
+ export declare function parseBooleanFlag(value: string | undefined, name: string): boolean;
11
+ export declare function readMcpAuditOptionsFromEnv(env: Record<string, string | undefined>): ThingdMcpAuditOptions | false;
12
+ export declare function readCliValue(args: string[], index: number, name: string): string;
13
+ export declare function ensureHttpRuntimeIsSafe(options: HttpRuntimeSafetyOptions): void;
14
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/mcp/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAElE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,GAAG,SAAS,CAU5F;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY7E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAejF;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,qBAAqB,GAAG,KAAK,CAgB/B;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAU/E"}
@@ -0,0 +1,67 @@
1
+ export function parseThingdDriver(value) {
2
+ if (!value) {
3
+ return undefined;
4
+ }
5
+ if (value === "memory" || value === "native") {
6
+ return value;
7
+ }
8
+ throw new Error(`Unsupported thingd driver: ${value}`);
9
+ }
10
+ export function parsePort(value, fallback) {
11
+ if (!value) {
12
+ return fallback;
13
+ }
14
+ const port = Number.parseInt(value, 10);
15
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
16
+ throw new Error(`Invalid port: ${value}`);
17
+ }
18
+ return port;
19
+ }
20
+ export function parseBooleanFlag(value, name) {
21
+ if (!value) {
22
+ return false;
23
+ }
24
+ const normalized = value.toLowerCase();
25
+ if (["1", "true", "yes", "on"].includes(normalized)) {
26
+ return true;
27
+ }
28
+ if (["0", "false", "no", "off"].includes(normalized)) {
29
+ return false;
30
+ }
31
+ throw new Error(`Invalid ${name}: expected true or false`);
32
+ }
33
+ export function readMcpAuditOptionsFromEnv(env) {
34
+ const enabled = env.THINGD_MCP_AUDIT === undefined
35
+ ? undefined
36
+ : parseBooleanFlag(env.THINGD_MCP_AUDIT, "THINGD_MCP_AUDIT");
37
+ if (enabled === false) {
38
+ return false;
39
+ }
40
+ return {
41
+ enabled,
42
+ actor: env.THINGD_MCP_ACTOR,
43
+ source: env.THINGD_MCP_SOURCE,
44
+ stream: env.THINGD_MCP_AUDIT_STREAM,
45
+ };
46
+ }
47
+ export function readCliValue(args, index, name) {
48
+ const value = args[index + 1];
49
+ if (!value) {
50
+ throw new Error(`${name} requires a value`);
51
+ }
52
+ return value;
53
+ }
54
+ export function ensureHttpRuntimeIsSafe(options) {
55
+ const authToken = options.authToken?.trim();
56
+ if (authToken || options.allowUnauthenticated || isLoopbackHost(options.host)) {
57
+ return;
58
+ }
59
+ throw new Error("THINGD_AUTH_TOKEN is required when the HTTP MCP runtime binds to a non-loopback host. Set THINGD_AUTH_TOKEN or THINGD_ALLOW_UNAUTHENTICATED=true for local-only experiments.");
60
+ }
61
+ function isLoopbackHost(host) {
62
+ const normalized = host.toLowerCase();
63
+ return (normalized === "localhost" ||
64
+ normalized === "127.0.0.1" ||
65
+ normalized === "::1" ||
66
+ normalized === "[::1]");
67
+ }
@@ -0,0 +1,25 @@
1
+ import { type Server } from "node:http";
2
+ import { type ThingdMcpAuditOptions, type ThingdMcpHardeningOptions } from "@thingd/node";
3
+ import { type ThingdClusterOptions } from "./cluster.js";
4
+ import { type ThingDStorageDriver } from "./config.js";
5
+ export type ThingdHttpServerOptions = {
6
+ path: string;
7
+ driver?: ThingDStorageDriver;
8
+ host?: string;
9
+ port?: number;
10
+ authToken?: string;
11
+ allowUnauthenticated?: boolean;
12
+ audit?: ThingdMcpAuditOptions | false;
13
+ cluster?: ThingdClusterOptions;
14
+ mcpPath?: string;
15
+ healthPath?: string;
16
+ hardening?: ThingdMcpHardeningOptions;
17
+ };
18
+ export type RunningThingdHttpServer = {
19
+ server: Server;
20
+ url: string;
21
+ mcpUrl: string;
22
+ close(): Promise<void>;
23
+ };
24
+ export declare function startThingdHttpServer(options: ThingdHttpServerOptions): Promise<RunningThingdHttpServer>;
25
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/mcp/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAGjG,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC/B,MAAM,cAAc,CAAC;AACtB,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IACtC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAkBF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,uBAAuB,CAAC,CAqDlC"}