snow-flow 11.0.0 → 11.0.2
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/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* MCP HTTP Server — Bun entry point
|
|
4
|
+
* ----------------------------------------------------------------------
|
|
5
|
+
* Starts the Hono-based MCP server on a fixed port and wires up an HTTP
|
|
6
|
+
* callback-based `ContextResolver` (see http-resolver.ts) so that tenant
|
|
7
|
+
* credential resolution lives in the deployment that owns the tenant DB
|
|
8
|
+
* (e.g. snow-flow-enterprise's portal backend), not in this image.
|
|
9
|
+
*
|
|
10
|
+
* Env vars (all required at runtime):
|
|
11
|
+
* MCP_RESOLVER_URL — full URL of the downstream resolver endpoint
|
|
12
|
+
* MCP_INTERNAL_TOKEN — shared secret sent on `X-Internal-Auth`
|
|
13
|
+
* MCP_HTTP_PORT — listen port (default 8082)
|
|
14
|
+
*
|
|
15
|
+
* Intended runtime: Bun. The image is built from `Dockerfile.mcp-http`
|
|
16
|
+
* and published as `ghcr.io/groeimetai/snow-flow-mcp-http`. Callers that
|
|
17
|
+
* need to self-host can also run it via `bun run http-entry.ts`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { toolRegistry } from "../shared/tool-registry.js"
|
|
21
|
+
import { createHttpApp } from "./http.js"
|
|
22
|
+
import { createHttpResolver } from "./http-resolver.js"
|
|
23
|
+
import { mcpDebug, mcpWarn } from "../../shared/mcp-debug.js"
|
|
24
|
+
|
|
25
|
+
async function main(): Promise<void> {
|
|
26
|
+
const resolverUrl = process.env.MCP_RESOLVER_URL
|
|
27
|
+
const internalToken = process.env.MCP_INTERNAL_TOKEN
|
|
28
|
+
const port = Number(process.env.MCP_HTTP_PORT ?? 8082)
|
|
29
|
+
|
|
30
|
+
if (!resolverUrl || !internalToken) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"mcp-http entry requires MCP_RESOLVER_URL and MCP_INTERNAL_TOKEN env vars. " +
|
|
33
|
+
"See transports/http-resolver.ts for the contract the endpoint must implement.",
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
mcpDebug("[mcp-http] discovering tools…")
|
|
38
|
+
const discovery = await toolRegistry.initialize()
|
|
39
|
+
mcpDebug(
|
|
40
|
+
`[mcp-http] tool discovery done: ${discovery.toolsRegistered} registered, ` +
|
|
41
|
+
`${discovery.toolsFailed} failed across ${discovery.domains.length} domains`,
|
|
42
|
+
)
|
|
43
|
+
if (discovery.toolsFailed > 0) {
|
|
44
|
+
for (const err of discovery.errors) {
|
|
45
|
+
mcpWarn(`[mcp-http] tool load failed: ${err.filePath} — ${err.error}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const app = createHttpApp({
|
|
50
|
+
resolveContext: createHttpResolver({ url: resolverUrl, internalToken }),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Bun's native server. Runs on Node too via `@hono/node-server`, but the
|
|
54
|
+
// published image is Bun-based so we use the built-in server here.
|
|
55
|
+
const server = (globalThis as any).Bun?.serve?.({ fetch: app.fetch, port })
|
|
56
|
+
if (!server) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Bun.serve is not available. This entry point is intended for the Bun runtime " +
|
|
59
|
+
"shipped with Dockerfile.mcp-http.",
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mcpDebug(`[mcp-http] listening on :${port}`)
|
|
64
|
+
mcpDebug(`[mcp-http] resolver endpoint: ${resolverUrl}`)
|
|
65
|
+
|
|
66
|
+
const shutdown = async (signal: string) => {
|
|
67
|
+
mcpDebug(`[mcp-http] received ${signal}, stopping…`)
|
|
68
|
+
try {
|
|
69
|
+
await server.stop?.(true)
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
process.exit(0)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process.on("SIGINT", () => {
|
|
77
|
+
shutdown("SIGINT").catch(() => process.exit(1))
|
|
78
|
+
})
|
|
79
|
+
process.on("SIGTERM", () => {
|
|
80
|
+
shutdown("SIGTERM").catch(() => process.exit(1))
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch((err: unknown) => {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.error(`[mcp-http] fatal: ${msg}`)
|
|
88
|
+
process.exit(1)
|
|
89
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic HTTP-callback context resolver.
|
|
3
|
+
*
|
|
4
|
+
* Called by the mcp-http container for every inbound MCP request. Instead
|
|
5
|
+
* of resolving the tenant + credentials locally (which would require the
|
|
6
|
+
* OS image to know about the portal DB schema + KMS), this helper forwards
|
|
7
|
+
* the inbound Bearer JWT to a caller-configured endpoint that performs the
|
|
8
|
+
* real resolution. The endpoint is expected to return a JSON body shaped
|
|
9
|
+
* exactly like `RequestContext`.
|
|
10
|
+
*
|
|
11
|
+
* Design intent
|
|
12
|
+
* -------------
|
|
13
|
+
* The OS repo ships the MCP infrastructure but knows nothing about how
|
|
14
|
+
* tenants are identified or where their ServiceNow credentials live. A
|
|
15
|
+
* downstream deployment (e.g. snow-flow-enterprise) runs this image as the
|
|
16
|
+
* `mcp-http` container, points `MCP_RESOLVER_URL` at its own HTTP endpoint,
|
|
17
|
+
* and hands out a shared-secret token so only the mcp-http container can
|
|
18
|
+
* call that endpoint.
|
|
19
|
+
*
|
|
20
|
+
* Env vars consumed by `createHttpResolver()`:
|
|
21
|
+
* MCP_RESOLVER_URL — full URL of the downstream resolver endpoint
|
|
22
|
+
* MCP_INTERNAL_TOKEN — shared secret the endpoint verifies on the
|
|
23
|
+
* `X-Internal-Auth` header. Keeps the endpoint
|
|
24
|
+
* closed to anyone else on the docker network.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { ContextResolver } from "../handlers/types.js"
|
|
28
|
+
|
|
29
|
+
export interface HttpResolverOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Full URL of the resolver endpoint. Example:
|
|
32
|
+
* http://portal:3000/api/internal/mcp-resolve
|
|
33
|
+
*/
|
|
34
|
+
url: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shared secret set in `X-Internal-Auth` on every call. Must match the
|
|
38
|
+
* value configured on the resolver endpoint side.
|
|
39
|
+
*/
|
|
40
|
+
internalToken: string
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-call timeout in ms. Defaults to 5000.
|
|
44
|
+
*/
|
|
45
|
+
timeoutMs?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a `ContextResolver` that forwards each inbound request to
|
|
50
|
+
* `opts.url`. The returned function is the exact type `createHttpApp`
|
|
51
|
+
* expects for its `resolveContext` dependency.
|
|
52
|
+
*/
|
|
53
|
+
export const createHttpResolver = (opts: HttpResolverOptions): ContextResolver => {
|
|
54
|
+
const timeoutMs = opts.timeoutMs ?? 5000
|
|
55
|
+
|
|
56
|
+
return async (request: any) => {
|
|
57
|
+
const headers = (request as any)?.headers ?? {}
|
|
58
|
+
// Forward the caller's Bearer token verbatim — the resolver endpoint
|
|
59
|
+
// is the thing that knows how to verify it.
|
|
60
|
+
const authorization =
|
|
61
|
+
headers.authorization ?? headers.Authorization ?? headers.AUTHORIZATION ?? ""
|
|
62
|
+
|
|
63
|
+
const controller = new AbortController()
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(opts.url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"X-Internal-Auth": opts.internalToken,
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({ authorization }),
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const body = await response.text().catch(() => "")
|
|
79
|
+
throw new Error(
|
|
80
|
+
`mcp-resolver endpoint returned ${response.status}: ${body.slice(0, 200)}`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (await response.json()) as Awaited<ReturnType<ContextResolver>>
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|