thingd-cli 0.3.0 → 0.4.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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/mcp/audit.d.ts +27 -0
- package/dist/mcp/audit.d.ts.map +1 -0
- package/dist/mcp/audit.js +36 -0
- package/dist/mcp/cluster.d.ts +44 -0
- package/dist/mcp/cluster.d.ts.map +1 -0
- package/dist/mcp/cluster.js +183 -0
- package/dist/mcp/config.d.ts +15 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +67 -0
- package/dist/mcp/http.d.ts +24 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +213 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/result.d.ts +3 -0
- package/dist/mcp/result.d.ts.map +1 -0
- package/dist/mcp/result.js +10 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +12 -0
- package/dist/mcp/tools.d.ts +8 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +314 -0
- package/dist/mcp-http.d.ts +3 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +36 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +3 -3
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAKA,OAAO,EAOL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,QAAQ,CAAC;AAIhB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAKA,OAAO,EAOL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,QAAQ,CAAC;AAIhB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAyCF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiCjB;AAyUD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAcf;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CAYxE"}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ Admin and operator CLI for thingd.
|
|
|
12
12
|
Usage:
|
|
13
13
|
thingd status [--url <url>]
|
|
14
14
|
thingd tools --url <url>
|
|
15
|
+
thingd mcp [--path <path>] [--driver <driver>]
|
|
16
|
+
thingd mcp-http [--path <path>] [--driver <driver>] [--host <host>] [--port <port>] [--auth-token <tok>] [--allow-unauthenticated]
|
|
15
17
|
thingd search <query> [--collection <name>] [--limit <n>]
|
|
16
18
|
thingd objects get <collection> <id>
|
|
17
19
|
thingd objects put <collection> <id> --text <text>
|
|
@@ -39,7 +41,7 @@ Options:
|
|
|
39
41
|
--limit <n> result limit for search and list commands
|
|
40
42
|
-h, --help show help
|
|
41
43
|
`;
|
|
42
|
-
const BOOLEAN_FLAGS = new Set(["h", "help", "json", "pretty"]);
|
|
44
|
+
const BOOLEAN_FLAGS = new Set(["h", "help", "json", "pretty", "allow-unauthenticated"]);
|
|
43
45
|
export async function runCli(args = process.argv.slice(2), options = {}) {
|
|
44
46
|
const parsed = parseArgs(args);
|
|
45
47
|
const context = {
|
|
@@ -86,6 +88,11 @@ async function runCommand(context) {
|
|
|
86
88
|
await runMcp(context);
|
|
87
89
|
return;
|
|
88
90
|
}
|
|
91
|
+
if (command === "mcp-http") {
|
|
92
|
+
const { runMcpHttp } = await import("./mcp-http.js");
|
|
93
|
+
await runMcpHttp(context);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
89
96
|
if (command === "objects") {
|
|
90
97
|
await runObjects(context);
|
|
91
98
|
return;
|
|
@@ -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,44 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export type ThingdClusterMode = "single" | "leader" | "follower";
|
|
3
|
+
export type ThingdClusterDiscovery = "none" | "static" | "kubernetes";
|
|
4
|
+
export type ThingdClusterOptions = {
|
|
5
|
+
mode?: ThingdClusterMode;
|
|
6
|
+
advertiseUrl?: string;
|
|
7
|
+
leaderUrl?: string;
|
|
8
|
+
peers?: string[];
|
|
9
|
+
discovery?: ThingdClusterDiscovery;
|
|
10
|
+
service?: string;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
port?: number;
|
|
13
|
+
forwardAuthToken?: string;
|
|
14
|
+
statusPath?: string;
|
|
15
|
+
peersPath?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ResolvedThingdClusterOptions = {
|
|
18
|
+
mode: ThingdClusterMode;
|
|
19
|
+
advertiseUrl?: string;
|
|
20
|
+
leaderUrl?: string;
|
|
21
|
+
peers: string[];
|
|
22
|
+
discovery: ThingdClusterDiscovery;
|
|
23
|
+
service?: string;
|
|
24
|
+
namespace?: string;
|
|
25
|
+
port: number;
|
|
26
|
+
forwardAuthToken?: string;
|
|
27
|
+
statusPath: string;
|
|
28
|
+
peersPath: string;
|
|
29
|
+
};
|
|
30
|
+
export type ThingdClusterStatus = {
|
|
31
|
+
mode: ThingdClusterMode;
|
|
32
|
+
writable: boolean;
|
|
33
|
+
forwarding: boolean;
|
|
34
|
+
leaderUrl?: string;
|
|
35
|
+
advertiseUrl?: string;
|
|
36
|
+
discovery: ThingdClusterDiscovery;
|
|
37
|
+
peers: string[];
|
|
38
|
+
replication: "not-implemented";
|
|
39
|
+
};
|
|
40
|
+
export declare function readClusterOptionsFromEnv(env: Record<string, string | undefined>): ThingdClusterOptions;
|
|
41
|
+
export declare function resolveClusterOptions(options: ThingdClusterOptions | undefined): ResolvedThingdClusterOptions;
|
|
42
|
+
export declare function clusterStatus(cluster: ResolvedThingdClusterOptions): ThingdClusterStatus;
|
|
43
|
+
export declare function forwardMcpRequestToLeader(cluster: ResolvedThingdClusterOptions, mcpPath: string, request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=cluster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/mcp/cluster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AACjE,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,IAAI,EAAE,iBAAiB,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,sBAAsB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,sBAAsB,CAAC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAIF,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,oBAAoB,CAYtB;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACxC,4BAA4B,CA6B9B;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,4BAA4B,GAAG,mBAAmB,CAWxF;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,4BAA4B,EACrC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { 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
|
+
peers: parsePeers(env.THINGD_CLUSTER_PEERS),
|
|
9
|
+
discovery: parseClusterDiscovery(env.THINGD_CLUSTER_DISCOVERY),
|
|
10
|
+
service: env.THINGD_CLUSTER_SERVICE,
|
|
11
|
+
namespace: env.THINGD_CLUSTER_NAMESPACE,
|
|
12
|
+
port: parsePort(env.THINGD_CLUSTER_PORT, DEFAULT_CLUSTER_PORT),
|
|
13
|
+
forwardAuthToken: env.THINGD_CLUSTER_FORWARD_AUTH_TOKEN ?? env.THINGD_AUTH_TOKEN,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function resolveClusterOptions(options) {
|
|
17
|
+
const mode = options?.mode ?? "single";
|
|
18
|
+
const discovery = options?.discovery ?? (options?.peers?.length ? "static" : "none");
|
|
19
|
+
const port = options?.port ?? DEFAULT_CLUSTER_PORT;
|
|
20
|
+
const peers = resolvePeers({
|
|
21
|
+
discovery,
|
|
22
|
+
peers: options?.peers ?? [],
|
|
23
|
+
service: options?.service,
|
|
24
|
+
namespace: options?.namespace,
|
|
25
|
+
port,
|
|
26
|
+
});
|
|
27
|
+
if (mode === "follower" && !options?.leaderUrl) {
|
|
28
|
+
throw new Error("THINGD_CLUSTER_LEADER_URL is required when THINGD_CLUSTER_MODE=follower");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
mode,
|
|
32
|
+
advertiseUrl: options?.advertiseUrl,
|
|
33
|
+
leaderUrl: options?.leaderUrl,
|
|
34
|
+
peers,
|
|
35
|
+
discovery,
|
|
36
|
+
service: options?.service,
|
|
37
|
+
namespace: options?.namespace,
|
|
38
|
+
port,
|
|
39
|
+
forwardAuthToken: options?.forwardAuthToken,
|
|
40
|
+
statusPath: options?.statusPath ?? "/cluster/status",
|
|
41
|
+
peersPath: options?.peersPath ?? "/cluster/peers",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function clusterStatus(cluster) {
|
|
45
|
+
return {
|
|
46
|
+
mode: cluster.mode,
|
|
47
|
+
writable: cluster.mode !== "follower",
|
|
48
|
+
forwarding: cluster.mode === "follower",
|
|
49
|
+
leaderUrl: cluster.leaderUrl,
|
|
50
|
+
advertiseUrl: cluster.advertiseUrl,
|
|
51
|
+
discovery: cluster.discovery,
|
|
52
|
+
peers: cluster.peers,
|
|
53
|
+
replication: "not-implemented",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function forwardMcpRequestToLeader(cluster, mcpPath, request, response) {
|
|
57
|
+
if (!cluster.leaderUrl) {
|
|
58
|
+
writeForwardError(response, 503, "cluster_leader_unavailable");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const upstreamUrl = leaderMcpUrl(cluster.leaderUrl, mcpPath);
|
|
62
|
+
const body = await readRequestBody(request);
|
|
63
|
+
const upstream = await fetch(upstreamUrl, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: forwardedHeaders(request, cluster.forwardAuthToken),
|
|
66
|
+
body: new Uint8Array(body),
|
|
67
|
+
});
|
|
68
|
+
response.writeHead(upstream.status, responseHeaders(upstream.headers));
|
|
69
|
+
response.end(Buffer.from(await upstream.arrayBuffer()));
|
|
70
|
+
}
|
|
71
|
+
function parseClusterMode(value) {
|
|
72
|
+
if (!value) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (value === "single" || value === "leader" || value === "follower") {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unsupported THINGD_CLUSTER_MODE: ${value}`);
|
|
79
|
+
}
|
|
80
|
+
function parseClusterDiscovery(value) {
|
|
81
|
+
if (!value) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (value === "none" || value === "static" || value === "kubernetes") {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Unsupported THINGD_CLUSTER_DISCOVERY: ${value}`);
|
|
88
|
+
}
|
|
89
|
+
function parsePeers(value) {
|
|
90
|
+
if (!value) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return value
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((peer) => peer.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
function resolvePeers(options) {
|
|
99
|
+
if (options.discovery === "static") {
|
|
100
|
+
return options.peers;
|
|
101
|
+
}
|
|
102
|
+
if (options.discovery !== "kubernetes" || !options.service) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const namespace = options.namespace ?? "default";
|
|
106
|
+
return [`http://${options.service}.${namespace}.svc.cluster.local:${options.port}`];
|
|
107
|
+
}
|
|
108
|
+
function leaderMcpUrl(leaderUrl, mcpPath) {
|
|
109
|
+
const url = new URL(leaderUrl);
|
|
110
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
111
|
+
url.pathname = mcpPath;
|
|
112
|
+
}
|
|
113
|
+
return url.toString();
|
|
114
|
+
}
|
|
115
|
+
function forwardedHeaders(request, forwardAuthToken) {
|
|
116
|
+
const headers = new Headers();
|
|
117
|
+
const contentType = request.headers["content-type"];
|
|
118
|
+
const protocolVersion = request.headers["mcp-protocol-version"];
|
|
119
|
+
const accept = request.headers.accept;
|
|
120
|
+
if (typeof contentType === "string") {
|
|
121
|
+
headers.set("Content-Type", contentType);
|
|
122
|
+
}
|
|
123
|
+
if (typeof accept === "string") {
|
|
124
|
+
headers.set("Accept", accept);
|
|
125
|
+
}
|
|
126
|
+
if (typeof protocolVersion === "string") {
|
|
127
|
+
headers.set("MCP-Protocol-Version", protocolVersion);
|
|
128
|
+
}
|
|
129
|
+
if (forwardAuthToken) {
|
|
130
|
+
headers.set("Authorization", `Bearer ${forwardAuthToken}`);
|
|
131
|
+
}
|
|
132
|
+
else if (typeof request.headers.authorization === "string") {
|
|
133
|
+
headers.set("Authorization", request.headers.authorization);
|
|
134
|
+
}
|
|
135
|
+
return headers;
|
|
136
|
+
}
|
|
137
|
+
function responseHeaders(headers) {
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const [key, value] of headers) {
|
|
140
|
+
if (!isHopByHopHeader(key)) {
|
|
141
|
+
result[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
function isHopByHopHeader(header) {
|
|
147
|
+
return [
|
|
148
|
+
"connection",
|
|
149
|
+
"content-length",
|
|
150
|
+
"keep-alive",
|
|
151
|
+
"proxy-authenticate",
|
|
152
|
+
"proxy-authorization",
|
|
153
|
+
"te",
|
|
154
|
+
"trailer",
|
|
155
|
+
"transfer-encoding",
|
|
156
|
+
"upgrade",
|
|
157
|
+
].includes(header.toLowerCase());
|
|
158
|
+
}
|
|
159
|
+
function readRequestBody(request) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const chunks = [];
|
|
162
|
+
request.on("data", (chunk) => {
|
|
163
|
+
chunks.push(chunk);
|
|
164
|
+
});
|
|
165
|
+
request.on("end", () => {
|
|
166
|
+
resolve(Buffer.concat(chunks));
|
|
167
|
+
});
|
|
168
|
+
request.on("error", reject);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function writeForwardError(response, statusCode, error) {
|
|
172
|
+
response.writeHead(statusCode, {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
});
|
|
175
|
+
response.end(JSON.stringify({
|
|
176
|
+
jsonrpc: "2.0",
|
|
177
|
+
error: {
|
|
178
|
+
code: -32_603,
|
|
179
|
+
message: error,
|
|
180
|
+
},
|
|
181
|
+
id: null,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ThingDDriver } from "thingd";
|
|
2
|
+
import type { ThingdMcpAuditOptions } from "./audit.js";
|
|
3
|
+
export type ThingDStorageDriver = Exclude<ThingDDriver, "remote">;
|
|
4
|
+
export type HttpRuntimeSafetyOptions = {
|
|
5
|
+
host: string;
|
|
6
|
+
authToken?: string;
|
|
7
|
+
allowUnauthenticated?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseThingdDriver(value: string | undefined): ThingDStorageDriver | undefined;
|
|
10
|
+
export declare function parsePort(value: string | undefined, fallback: number): number;
|
|
11
|
+
export declare function parseBooleanFlag(value: string | undefined, name: string): boolean;
|
|
12
|
+
export declare function readMcpAuditOptionsFromEnv(env: Record<string, string | undefined>): ThingdMcpAuditOptions | false;
|
|
13
|
+
export declare function readCliValue(args: string[], index: number, name: string): string;
|
|
14
|
+
export declare function ensureHttpRuntimeIsSafe(options: HttpRuntimeSafetyOptions): void;
|
|
15
|
+
//# 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,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,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,24 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { ThingdMcpAuditOptions } from "./audit.js";
|
|
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
|
+
};
|
|
17
|
+
export type RunningThingdHttpServer = {
|
|
18
|
+
server: Server;
|
|
19
|
+
url: string;
|
|
20
|
+
mcpUrl: string;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export declare function startThingdHttpServer(options: ThingdHttpServerOptions): Promise<RunningThingdHttpServer>;
|
|
24
|
+
//# 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,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGhF,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;CACrB,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;AAYF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,uBAAuB,CAAC,CAwClC"}
|
package/dist/mcp/http.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { ThingD } from "thingd";
|
|
4
|
+
import { clusterStatus, forwardMcpRequestToLeader, resolveClusterOptions, } from "./cluster.js";
|
|
5
|
+
import { ensureHttpRuntimeIsSafe } from "./config.js";
|
|
6
|
+
import { createThingdMcpServer } from "./server.js";
|
|
7
|
+
export async function startThingdHttpServer(options) {
|
|
8
|
+
const host = options.host ?? "127.0.0.1";
|
|
9
|
+
const port = options.port ?? 8757;
|
|
10
|
+
ensureHttpRuntimeIsSafe({
|
|
11
|
+
host,
|
|
12
|
+
authToken: options.authToken,
|
|
13
|
+
allowUnauthenticated: options.allowUnauthenticated,
|
|
14
|
+
});
|
|
15
|
+
const db = await ThingD.open({
|
|
16
|
+
path: options.path,
|
|
17
|
+
driver: options.driver,
|
|
18
|
+
});
|
|
19
|
+
const cluster = resolveClusterOptions(options.cluster);
|
|
20
|
+
const state = {
|
|
21
|
+
db,
|
|
22
|
+
authToken: options.authToken,
|
|
23
|
+
mcpPath: options.mcpPath ?? "/mcp",
|
|
24
|
+
healthPath: options.healthPath ?? "/healthz",
|
|
25
|
+
driver: options.driver ?? "memory",
|
|
26
|
+
audit: options.audit,
|
|
27
|
+
cluster,
|
|
28
|
+
};
|
|
29
|
+
const server = createServer((request, response) => {
|
|
30
|
+
void handleRequest(state, request, response);
|
|
31
|
+
});
|
|
32
|
+
await listen(server, port, host);
|
|
33
|
+
const address = server.address();
|
|
34
|
+
const resolvedPort = typeof address === "object" && address ? address.port : port;
|
|
35
|
+
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
36
|
+
const url = `http://${displayHost}:${resolvedPort}`;
|
|
37
|
+
return {
|
|
38
|
+
server,
|
|
39
|
+
url,
|
|
40
|
+
mcpUrl: `${url}${state.mcpPath}`,
|
|
41
|
+
close: () => close(server),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function handleRequest(state, request, response) {
|
|
45
|
+
try {
|
|
46
|
+
setCommonHeaders(response);
|
|
47
|
+
const path = requestPath(request);
|
|
48
|
+
if (request.method === "OPTIONS") {
|
|
49
|
+
response.writeHead(204);
|
|
50
|
+
response.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (path === state.healthPath) {
|
|
54
|
+
handleHealth(state, request, response);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (path === state.cluster.statusPath) {
|
|
58
|
+
handleClusterStatus(state, request, response);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (path === state.cluster.peersPath) {
|
|
62
|
+
handleClusterPeers(state, request, response);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (path !== state.mcpPath) {
|
|
66
|
+
writeJson(response, 404, {
|
|
67
|
+
error: "not_found",
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!isAuthorized(state, request)) {
|
|
72
|
+
response.setHeader("WWW-Authenticate", "Bearer");
|
|
73
|
+
writeJson(response, 401, {
|
|
74
|
+
error: "unauthorized",
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (request.method !== "POST") {
|
|
79
|
+
response.setHeader("Allow", "POST, OPTIONS");
|
|
80
|
+
writeJson(response, 405, {
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
error: {
|
|
83
|
+
code: -32_000,
|
|
84
|
+
message: "Method not allowed.",
|
|
85
|
+
},
|
|
86
|
+
id: null,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (state.cluster.mode === "follower") {
|
|
91
|
+
await forwardMcpRequestToLeader(state.cluster, state.mcpPath, request, response);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await handleMcpRequest(state, request, response);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!response.headersSent) {
|
|
98
|
+
writeJson(response, 500, {
|
|
99
|
+
jsonrpc: "2.0",
|
|
100
|
+
error: {
|
|
101
|
+
code: -32_603,
|
|
102
|
+
message: error instanceof Error ? error.message : "Internal server error",
|
|
103
|
+
},
|
|
104
|
+
id: null,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
response.end();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function handleHealth(state, request, response) {
|
|
112
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
113
|
+
response.setHeader("Allow", "GET, HEAD, OPTIONS");
|
|
114
|
+
writeJson(response, 405, {
|
|
115
|
+
error: "method_not_allowed",
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
writeJson(response, 200, {
|
|
120
|
+
ok: true,
|
|
121
|
+
service: "thingd-mcp",
|
|
122
|
+
driver: state.driver,
|
|
123
|
+
mcpPath: state.mcpPath,
|
|
124
|
+
cluster: clusterStatus(state.cluster),
|
|
125
|
+
}, request.method === "HEAD");
|
|
126
|
+
}
|
|
127
|
+
function handleClusterStatus(state, request, response) {
|
|
128
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
129
|
+
response.setHeader("Allow", "GET, HEAD, OPTIONS");
|
|
130
|
+
writeJson(response, 405, {
|
|
131
|
+
error: "method_not_allowed",
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
writeJson(response, 200, clusterStatus(state.cluster), request.method === "HEAD");
|
|
136
|
+
}
|
|
137
|
+
function handleClusterPeers(state, request, response) {
|
|
138
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
139
|
+
response.setHeader("Allow", "GET, HEAD, OPTIONS");
|
|
140
|
+
writeJson(response, 405, {
|
|
141
|
+
error: "method_not_allowed",
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
writeJson(response, 200, {
|
|
146
|
+
peers: state.cluster.peers,
|
|
147
|
+
discovery: state.cluster.discovery,
|
|
148
|
+
}, request.method === "HEAD");
|
|
149
|
+
}
|
|
150
|
+
async function handleMcpRequest(state, request, response) {
|
|
151
|
+
const server = createThingdMcpServer(state.db, {
|
|
152
|
+
audit: state.audit,
|
|
153
|
+
});
|
|
154
|
+
const transport = new StreamableHTTPServerTransport({
|
|
155
|
+
sessionIdGenerator: undefined,
|
|
156
|
+
});
|
|
157
|
+
response.on("close", () => {
|
|
158
|
+
void transport.close();
|
|
159
|
+
void server.close();
|
|
160
|
+
});
|
|
161
|
+
await server.connect(transport);
|
|
162
|
+
await transport.handleRequest(request, response);
|
|
163
|
+
}
|
|
164
|
+
function requestPath(request) {
|
|
165
|
+
return new URL(request.url ?? "/", "http://thingd.local").pathname;
|
|
166
|
+
}
|
|
167
|
+
function isAuthorized(state, request) {
|
|
168
|
+
if (!state.authToken) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return request.headers.authorization === `Bearer ${state.authToken}`;
|
|
172
|
+
}
|
|
173
|
+
function setCommonHeaders(response) {
|
|
174
|
+
response.setHeader("Access-Control-Allow-Origin", "*");
|
|
175
|
+
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, MCP-Protocol-Version");
|
|
176
|
+
response.setHeader("Access-Control-Allow-Methods", "POST, GET, HEAD, OPTIONS");
|
|
177
|
+
}
|
|
178
|
+
function writeJson(response, statusCode, body, headersOnly = false) {
|
|
179
|
+
response.writeHead(statusCode, {
|
|
180
|
+
"Content-Type": "application/json",
|
|
181
|
+
});
|
|
182
|
+
if (headersOnly) {
|
|
183
|
+
response.end();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
response.end(JSON.stringify(body));
|
|
187
|
+
}
|
|
188
|
+
function listen(server, port, host) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const onError = (error) => {
|
|
191
|
+
server.off("listening", onListening);
|
|
192
|
+
reject(error);
|
|
193
|
+
};
|
|
194
|
+
const onListening = () => {
|
|
195
|
+
server.off("error", onError);
|
|
196
|
+
resolve();
|
|
197
|
+
};
|
|
198
|
+
server.once("error", onError);
|
|
199
|
+
server.once("listening", onListening);
|
|
200
|
+
server.listen(port, host);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function close(server) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
server.close((error) => {
|
|
206
|
+
if (error) {
|
|
207
|
+
reject(error);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
resolve();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { ThingdMcpAuditMetadata, ThingdMcpAuditOptions } from "./audit.js";
|
|
2
|
+
export type { ResolvedThingdClusterOptions, ThingdClusterDiscovery, ThingdClusterMode, ThingdClusterOptions, ThingdClusterStatus, } from "./cluster.js";
|
|
3
|
+
export type { RunningThingdHttpServer, ThingdHttpServerOptions } from "./http.js";
|
|
4
|
+
export { startThingdHttpServer } from "./http.js";
|
|
5
|
+
export type { ThingdMcpServerOptions } from "./server.js";
|
|
6
|
+
export { createThingdMcpServer } from "./server.js";
|
|
7
|
+
export type { RegisterThingdToolsOptions } from "./tools.js";
|
|
8
|
+
export { registerThingdTools } from "./tools.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAChF,YAAY,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/mcp/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CASzD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ThingD } from "thingd";
|
|
3
|
+
import type { ThingdMcpAuditOptions } from "./audit.js";
|
|
4
|
+
export type ThingdMcpServerOptions = {
|
|
5
|
+
audit?: ThingdMcpAuditOptions | false;
|
|
6
|
+
};
|
|
7
|
+
export declare function createThingdMcpServer(db: ThingD, options?: ThingdMcpServerOptions): McpServer;
|
|
8
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;CACvC,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAcjG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerThingdTools } from "./tools.js";
|
|
3
|
+
export function createThingdMcpServer(db, options = {}) {
|
|
4
|
+
const server = new McpServer({
|
|
5
|
+
name: "thingd",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
}, {
|
|
8
|
+
instructions: "Use thingd tools to search, read, write, and queue work in an object-shaped local memory store. Prefer searching before writing duplicate memory.",
|
|
9
|
+
});
|
|
10
|
+
registerThingdTools(server, db, options);
|
|
11
|
+
return server;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ThingD } from "thingd";
|
|
3
|
+
import { type ThingdMcpAuditOptions } from "./audit.js";
|
|
4
|
+
export type RegisterThingdToolsOptions = {
|
|
5
|
+
audit?: ThingdMcpAuditOptions | false;
|
|
6
|
+
};
|
|
7
|
+
export declare function registerThingdTools(server: McpServer, db: ThingD, options?: RegisterThingdToolsOptions): void;
|
|
8
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAC;AAWpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;CACvC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,0BAA+B,GACvC,IAAI,CA6WN"}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { appendMcpAuditEvent, resolveThingdMcpAuditOptions, } from "./audit.js";
|
|
3
|
+
import { jsonResult } from "./result.js";
|
|
4
|
+
const memoryObjectSchema = z.object({ id: z.string().min(1) }).catchall(z.unknown());
|
|
5
|
+
const memoryEventSchema = z.object({ type: z.string().min(1) }).catchall(z.unknown());
|
|
6
|
+
const objectPayloadSchema = z.record(z.string(), z.unknown());
|
|
7
|
+
const auditInputSchema = {
|
|
8
|
+
actor: z.string().min(1).optional(),
|
|
9
|
+
source: z.string().min(1).optional(),
|
|
10
|
+
};
|
|
11
|
+
export function registerThingdTools(server, db, options = {}) {
|
|
12
|
+
const audit = resolveThingdMcpAuditOptions(options.audit);
|
|
13
|
+
server.registerTool("thing.search", {
|
|
14
|
+
title: "Search Memory",
|
|
15
|
+
description: "Search thingd objects and events by text.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
query: z.string().min(1),
|
|
18
|
+
collections: z.array(z.string().min(1)).optional(),
|
|
19
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
20
|
+
},
|
|
21
|
+
annotations: {
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: false,
|
|
26
|
+
},
|
|
27
|
+
}, async ({ query, collections, limit }) => jsonResult(await db.search(query, { collections, limit })));
|
|
28
|
+
server.registerTool("thing.get", {
|
|
29
|
+
title: "Get Object",
|
|
30
|
+
description: "Read one thingd object by collection and id.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
collection: z.string().min(1),
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
},
|
|
35
|
+
annotations: {
|
|
36
|
+
readOnlyHint: true,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: true,
|
|
39
|
+
openWorldHint: false,
|
|
40
|
+
},
|
|
41
|
+
}, async ({ collection, id }) => jsonResult(await db.get(collection, id)));
|
|
42
|
+
server.registerTool("thing.put", {
|
|
43
|
+
title: "Put Object",
|
|
44
|
+
description: "Create or replace one object-shaped memory record.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
collection: z.string().min(1),
|
|
47
|
+
object: memoryObjectSchema,
|
|
48
|
+
...auditInputSchema,
|
|
49
|
+
},
|
|
50
|
+
annotations: {
|
|
51
|
+
readOnlyHint: false,
|
|
52
|
+
destructiveHint: false,
|
|
53
|
+
idempotentHint: false,
|
|
54
|
+
openWorldHint: false,
|
|
55
|
+
},
|
|
56
|
+
}, async ({ collection, object, actor, source }) => {
|
|
57
|
+
const stored = await db.put(collection, object);
|
|
58
|
+
await appendMcpAuditEvent(db, audit, {
|
|
59
|
+
action: "objects.put",
|
|
60
|
+
target: {
|
|
61
|
+
collection,
|
|
62
|
+
id: stored.id,
|
|
63
|
+
},
|
|
64
|
+
metadata: auditMetadata(actor, source),
|
|
65
|
+
result: {
|
|
66
|
+
collection: stored.collection,
|
|
67
|
+
id: stored.id,
|
|
68
|
+
version: stored.version,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return jsonResult(stored);
|
|
72
|
+
});
|
|
73
|
+
server.registerTool("thing.delete", {
|
|
74
|
+
title: "Delete Object",
|
|
75
|
+
description: "Delete one thingd object by collection and id.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
collection: z.string().min(1),
|
|
78
|
+
id: z.string().min(1),
|
|
79
|
+
...auditInputSchema,
|
|
80
|
+
},
|
|
81
|
+
annotations: {
|
|
82
|
+
readOnlyHint: false,
|
|
83
|
+
destructiveHint: true,
|
|
84
|
+
idempotentHint: true,
|
|
85
|
+
openWorldHint: false,
|
|
86
|
+
},
|
|
87
|
+
}, async ({ collection, id, actor, source }) => {
|
|
88
|
+
const result = await db.delete(collection, id);
|
|
89
|
+
await appendMcpAuditEvent(db, audit, {
|
|
90
|
+
action: "objects.delete",
|
|
91
|
+
target: {
|
|
92
|
+
collection,
|
|
93
|
+
id,
|
|
94
|
+
},
|
|
95
|
+
metadata: auditMetadata(actor, source),
|
|
96
|
+
result,
|
|
97
|
+
});
|
|
98
|
+
return jsonResult(result);
|
|
99
|
+
});
|
|
100
|
+
server.registerTool("thing.events.append", {
|
|
101
|
+
title: "Append Event",
|
|
102
|
+
description: "Append an event to a thingd stream.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
stream: z.string().min(1),
|
|
105
|
+
event: memoryEventSchema,
|
|
106
|
+
...auditInputSchema,
|
|
107
|
+
},
|
|
108
|
+
annotations: {
|
|
109
|
+
readOnlyHint: false,
|
|
110
|
+
destructiveHint: false,
|
|
111
|
+
idempotentHint: false,
|
|
112
|
+
openWorldHint: false,
|
|
113
|
+
},
|
|
114
|
+
}, async ({ stream, event, actor, source }) => {
|
|
115
|
+
const stored = await db.events.append(stream, event);
|
|
116
|
+
await appendMcpAuditEvent(db, audit, {
|
|
117
|
+
action: "events.append",
|
|
118
|
+
target: {
|
|
119
|
+
stream,
|
|
120
|
+
eventType: stored.type,
|
|
121
|
+
eventId: stored.id,
|
|
122
|
+
},
|
|
123
|
+
metadata: auditMetadata(actor, source),
|
|
124
|
+
result: {
|
|
125
|
+
id: stored.id,
|
|
126
|
+
stream: stored.stream,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
return jsonResult(stored);
|
|
130
|
+
});
|
|
131
|
+
server.registerTool("thing.events.list", {
|
|
132
|
+
title: "List Events",
|
|
133
|
+
description: "List thingd events, optionally filtered by stream.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
stream: z.string().min(1).optional(),
|
|
136
|
+
},
|
|
137
|
+
annotations: {
|
|
138
|
+
readOnlyHint: true,
|
|
139
|
+
destructiveHint: false,
|
|
140
|
+
idempotentHint: true,
|
|
141
|
+
openWorldHint: false,
|
|
142
|
+
},
|
|
143
|
+
}, async ({ stream }) => jsonResult(await db.events.list(stream)));
|
|
144
|
+
server.registerTool("thing.queue.push", {
|
|
145
|
+
title: "Push Queue Job",
|
|
146
|
+
description: "Push a durable job onto a thingd queue.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
queue: z.string().min(1),
|
|
149
|
+
payload: objectPayloadSchema,
|
|
150
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
151
|
+
maxAttempts: z.number().int().positive().max(100).optional(),
|
|
152
|
+
delayMs: z.number().int().min(0).optional(),
|
|
153
|
+
...auditInputSchema,
|
|
154
|
+
},
|
|
155
|
+
annotations: {
|
|
156
|
+
readOnlyHint: false,
|
|
157
|
+
destructiveHint: false,
|
|
158
|
+
idempotentHint: false,
|
|
159
|
+
openWorldHint: false,
|
|
160
|
+
},
|
|
161
|
+
}, async ({ queue, payload, idempotencyKey, maxAttempts, delayMs, actor, source }) => {
|
|
162
|
+
const job = await db.queue(queue).push(payload, {
|
|
163
|
+
idempotencyKey,
|
|
164
|
+
maxAttempts,
|
|
165
|
+
delayMs,
|
|
166
|
+
});
|
|
167
|
+
await appendMcpAuditEvent(db, audit, {
|
|
168
|
+
action: "queue.push",
|
|
169
|
+
target: {
|
|
170
|
+
queue,
|
|
171
|
+
id: job.id,
|
|
172
|
+
},
|
|
173
|
+
metadata: auditMetadata(actor, source),
|
|
174
|
+
result: {
|
|
175
|
+
id: job.id,
|
|
176
|
+
queue: job.queue,
|
|
177
|
+
status: job.status,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return jsonResult(job);
|
|
181
|
+
});
|
|
182
|
+
server.registerTool("thing.queue.claim", {
|
|
183
|
+
title: "Claim Queue Job",
|
|
184
|
+
description: "Claim the next ready job from a thingd queue.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
queue: z.string().min(1),
|
|
187
|
+
leaseMs: z.number().int().optional(),
|
|
188
|
+
...auditInputSchema,
|
|
189
|
+
},
|
|
190
|
+
annotations: {
|
|
191
|
+
readOnlyHint: false,
|
|
192
|
+
destructiveHint: false,
|
|
193
|
+
idempotentHint: false,
|
|
194
|
+
openWorldHint: false,
|
|
195
|
+
},
|
|
196
|
+
}, async ({ queue, leaseMs, actor, source }) => {
|
|
197
|
+
const job = await db.queue(queue).claim({ leaseMs });
|
|
198
|
+
if (job) {
|
|
199
|
+
await appendMcpAuditEvent(db, audit, {
|
|
200
|
+
action: "queue.claim",
|
|
201
|
+
target: {
|
|
202
|
+
queue,
|
|
203
|
+
id: job.id,
|
|
204
|
+
},
|
|
205
|
+
metadata: auditMetadata(actor, source),
|
|
206
|
+
result: {
|
|
207
|
+
id: job.id,
|
|
208
|
+
queue: job.queue,
|
|
209
|
+
status: job.status,
|
|
210
|
+
attempts: job.attempts,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return jsonResult(job);
|
|
215
|
+
});
|
|
216
|
+
server.registerTool("thing.queue.ack", {
|
|
217
|
+
title: "Acknowledge Queue Job",
|
|
218
|
+
description: "Mark one leased queue job as completed.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
queue: z.string().min(1),
|
|
221
|
+
id: z.string().min(1),
|
|
222
|
+
...auditInputSchema,
|
|
223
|
+
},
|
|
224
|
+
annotations: {
|
|
225
|
+
readOnlyHint: false,
|
|
226
|
+
destructiveHint: false,
|
|
227
|
+
idempotentHint: false,
|
|
228
|
+
openWorldHint: false,
|
|
229
|
+
},
|
|
230
|
+
}, async ({ queue, id, actor, source }) => {
|
|
231
|
+
const result = await db.queue(queue).ack(id);
|
|
232
|
+
if (result.ok) {
|
|
233
|
+
await appendMcpAuditEvent(db, audit, {
|
|
234
|
+
action: "queue.ack",
|
|
235
|
+
target: {
|
|
236
|
+
queue,
|
|
237
|
+
id,
|
|
238
|
+
},
|
|
239
|
+
metadata: auditMetadata(actor, source),
|
|
240
|
+
result: {
|
|
241
|
+
ok: true,
|
|
242
|
+
status: result.job.status,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return jsonResult(result);
|
|
247
|
+
});
|
|
248
|
+
server.registerTool("thing.queue.nack", {
|
|
249
|
+
title: "Reject Queue Job",
|
|
250
|
+
description: "Reject a leased queue job for retry or dead-letter routing.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
queue: z.string().min(1),
|
|
253
|
+
id: z.string().min(1),
|
|
254
|
+
delayMs: z.number().int().min(0).optional(),
|
|
255
|
+
error: z.string().optional(),
|
|
256
|
+
...auditInputSchema,
|
|
257
|
+
},
|
|
258
|
+
annotations: {
|
|
259
|
+
readOnlyHint: false,
|
|
260
|
+
destructiveHint: false,
|
|
261
|
+
idempotentHint: false,
|
|
262
|
+
openWorldHint: false,
|
|
263
|
+
},
|
|
264
|
+
}, async ({ queue, id, delayMs, error, actor, source }) => {
|
|
265
|
+
const result = await db.queue(queue).nack(id, { delayMs, error });
|
|
266
|
+
if (result.ok) {
|
|
267
|
+
await appendMcpAuditEvent(db, audit, {
|
|
268
|
+
action: "queue.nack",
|
|
269
|
+
target: {
|
|
270
|
+
queue,
|
|
271
|
+
id,
|
|
272
|
+
},
|
|
273
|
+
metadata: auditMetadata(actor, source),
|
|
274
|
+
result: {
|
|
275
|
+
ok: true,
|
|
276
|
+
status: result.job.status,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return jsonResult(result);
|
|
281
|
+
});
|
|
282
|
+
server.registerTool("thing.queue.list", {
|
|
283
|
+
title: "List Queue Jobs",
|
|
284
|
+
description: "List jobs in a thingd queue.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
queue: z.string().min(1),
|
|
287
|
+
},
|
|
288
|
+
annotations: {
|
|
289
|
+
readOnlyHint: true,
|
|
290
|
+
destructiveHint: false,
|
|
291
|
+
idempotentHint: true,
|
|
292
|
+
openWorldHint: false,
|
|
293
|
+
},
|
|
294
|
+
}, async ({ queue }) => jsonResult(await db.queue(queue).list()));
|
|
295
|
+
server.registerTool("thing.queue.dead", {
|
|
296
|
+
title: "List Dead Queue Jobs",
|
|
297
|
+
description: "List dead-letter jobs in a thingd queue.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
queue: z.string().min(1),
|
|
300
|
+
},
|
|
301
|
+
annotations: {
|
|
302
|
+
readOnlyHint: true,
|
|
303
|
+
destructiveHint: false,
|
|
304
|
+
idempotentHint: true,
|
|
305
|
+
openWorldHint: false,
|
|
306
|
+
},
|
|
307
|
+
}, async ({ queue }) => jsonResult(await db.queue(queue).dead()));
|
|
308
|
+
}
|
|
309
|
+
function auditMetadata(actor, source) {
|
|
310
|
+
return {
|
|
311
|
+
actor,
|
|
312
|
+
source,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-http.d.ts","sourceRoot":"","sources":["../src/mcp-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK7C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCnE"}
|
package/dist/mcp-http.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readClusterOptionsFromEnv } from "./mcp/cluster.js";
|
|
2
|
+
import { parsePort, parseThingdDriver, readMcpAuditOptionsFromEnv } from "./mcp/config.js";
|
|
3
|
+
import { startThingdHttpServer } from "./mcp/http.js";
|
|
4
|
+
export async function runMcpHttp(context) {
|
|
5
|
+
const parsed = context.parsed;
|
|
6
|
+
// Resolve port, host, allowUnauthenticated, etc. from flags or env
|
|
7
|
+
const portStr = parsed.flags.get("port")?.at(-1) ?? context.env.THINGD_PORT;
|
|
8
|
+
const host = parsed.flags.get("host")?.at(-1) ?? context.env.THINGD_HOST ?? "127.0.0.1";
|
|
9
|
+
const path = parsed.flags.get("path")?.at(-1) ?? context.env.THINGD_PATH ?? ":memory:";
|
|
10
|
+
const driverStr = parsed.flags.get("driver")?.at(-1) ?? context.env.THINGD_DRIVER;
|
|
11
|
+
const authToken = parsed.flags.get("auth-token")?.at(-1) ?? context.env.THINGD_AUTH_TOKEN;
|
|
12
|
+
const allowUnauthenticated = parsed.booleans.has("allow-unauthenticated") || parsed.flags.has("allow-unauthenticated");
|
|
13
|
+
const port = parsePort(portStr, 8757);
|
|
14
|
+
const driver = parseThingdDriver(driverStr);
|
|
15
|
+
if (!authToken) {
|
|
16
|
+
console.error("thingd-mcp HTTP runtime is starting without THINGD_AUTH_TOKEN.");
|
|
17
|
+
}
|
|
18
|
+
const runtime = await startThingdHttpServer({
|
|
19
|
+
path,
|
|
20
|
+
driver,
|
|
21
|
+
host,
|
|
22
|
+
port,
|
|
23
|
+
authToken,
|
|
24
|
+
allowUnauthenticated,
|
|
25
|
+
audit: readMcpAuditOptionsFromEnv(context.env),
|
|
26
|
+
cluster: readClusterOptionsFromEnv(context.env),
|
|
27
|
+
});
|
|
28
|
+
console.error(`thingd MCP HTTP runtime listening at ${runtime.mcpUrl}`);
|
|
29
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
30
|
+
process.once(signal, () => {
|
|
31
|
+
void runtime.close().finally(() => process.exit(0));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Keep the process alive
|
|
35
|
+
return new Promise(() => { });
|
|
36
|
+
}
|
package/dist/mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAU,MAAM,YAAY,CAAC;AAGrD,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAY/D"}
|
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
-
import { createThingdMcpServer } from "thingd-mcp";
|
|
3
2
|
import { withDb } from "./index.js";
|
|
3
|
+
import { createThingdMcpServer } from "./mcp/index.js";
|
|
4
4
|
export async function runMcp(context) {
|
|
5
5
|
await withDb(context, async (db) => {
|
|
6
6
|
// We pass empty options to createThingdMcpServer so it uses default audit behavior
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thingd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Admin and operator CLI for thingd.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Sayan Mohsin",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"cli-table3": "^0.6.5",
|
|
34
34
|
"picocolors": "^1.1.1",
|
|
35
|
-
"
|
|
36
|
-
"thingd
|
|
35
|
+
"zod": "^4.4.3",
|
|
36
|
+
"thingd": "0.4.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=20"
|