@frontmcp/edge 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kv-cache.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * KV-backed last-good bundle cache for managed edge mode.
3
+ *
4
+ * A Cloudflare Worker has no filesystem, so the SaaS bundle source's default
5
+ * on-disk cache (the boot fallback when a fresh pull fails) can't be used.
6
+ * `createKvBundleCache` adapts a Cloudflare **KV namespace** to the generic
7
+ * `BundleCacheStore` contract (`@frontmcp/adapters/skills`) so the last-good
8
+ * bundle survives across isolate restarts and cold starts.
9
+ *
10
+ * ```ts
11
+ * import { createEdgeMcp, createKvBundleCache } from '@frontmcp/edge';
12
+ *
13
+ * export default createEdgeMcp({
14
+ * info: { name: 'my-worker', version: '1.0.0' },
15
+ * apps: [],
16
+ * tasks: { enabled: false },
17
+ * managed: {
18
+ * endpoint: env.BUNDLE_ENDPOINT,
19
+ * authToken: env.PULL_TOKEN,
20
+ * // ...
21
+ * cache: createKvBundleCache(env.BUNDLE_CACHE), // KV namespace binding
22
+ * },
23
+ * });
24
+ * ```
25
+ */
26
+ /**
27
+ * Minimal structural subset of the Cloudflare `KVNamespace` binding this cache
28
+ * needs. Declared locally so `@frontmcp/edge` stays free of a hard dependency
29
+ * on `@cloudflare/workers-types` (any runtime exposing this shape works).
30
+ */
31
+ export interface EdgeKvNamespace {
32
+ get(key: string, type?: 'text'): Promise<string | null>;
33
+ put(key: string, value: string, options?: {
34
+ expirationTtl?: number;
35
+ }): Promise<void>;
36
+ }
37
+ /**
38
+ * A last-good bundle cache. Structurally compatible with the adapters'
39
+ * `BundleCacheStore` — kept local (bundle typed as `unknown`) so the edge
40
+ * package doesn't depend on `@frontmcp/adapters`; the cache only round-trips
41
+ * JSON and never inspects the bundle's shape.
42
+ */
43
+ export interface EdgeBundleCacheStore {
44
+ read(): Promise<unknown>;
45
+ write(bundle: unknown): Promise<void>;
46
+ }
47
+ /** Tuning for {@link createKvBundleCache}. */
48
+ export interface KvBundleCacheOptions {
49
+ /** KV key for the serialized last-good bundle. Default `frontmcp:bundle:last-good`. */
50
+ key?: string;
51
+ /** Optional KV TTL (seconds). Omit to persist indefinitely (the usual choice — it's a fallback). */
52
+ expirationTtl?: number;
53
+ }
54
+ /**
55
+ * Lazily resolve a cache from the per-request Worker `env`. Cloudflare bindings
56
+ * (KV namespaces, etc.) live on the `env` argument passed to `fetch`/`scheduled`
57
+ * — NOT in module scope — so a cache that needs a binding must be built from
58
+ * `env` at request time, not when the module first evaluates.
59
+ */
60
+ export type EdgeBundleCacheFactory = (env: unknown) => EdgeBundleCacheStore | undefined;
61
+ /**
62
+ * Build a {@link EdgeBundleCacheStore} backed by a Cloudflare KV namespace.
63
+ *
64
+ * Reads tolerate a missing key (cold KV) and corrupt JSON by resolving to
65
+ * `undefined` — the source then treats it as "no last-good cache" and surfaces
66
+ * the original pull failure, rather than crashing on a bad cache entry.
67
+ */
68
+ export declare function createKvBundleCache(kv: EdgeKvNamespace, options?: KvBundleCacheOptions): EdgeBundleCacheStore;
69
+ /**
70
+ * Convenience {@link EdgeBundleCacheFactory}: read a KV namespace from
71
+ * `env[binding]` at request time and wrap it as a bundle cache. Pass this as
72
+ * `managed.cache` so the binding is resolved from the Worker's `env` (where CF
73
+ * bindings actually live) instead of at module-eval where it doesn't exist yet.
74
+ *
75
+ * ```ts
76
+ * managed: { …, cache: kvBundleCacheFromEnv('BUNDLE_CACHE') }
77
+ * ```
78
+ *
79
+ * Returns `undefined` (no cache → source uses no fallback) when the binding is
80
+ * absent, rather than throwing — a misconfigured binding shouldn't crash boot.
81
+ */
82
+ export declare function kvBundleCacheFromEnv(binding: string, options?: KvBundleCacheOptions): EdgeBundleCacheFactory;
package/managed.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Managed edge mode — an auto-updating, skilled-OpenAPI-backed edge server.
3
+ *
4
+ * Points the edge runtime at a SaaS endpoint that serves a signed
5
+ * **skilled-openapi** bundle (OpenAPI specs compiled into MCP skills + tools).
6
+ * The bundle is pulled on boot and kept fresh by polling (and/or a push
7
+ * webhook), so the server's capabilities update **without redeploying** —
8
+ * backed by the existing `@frontmcp/plugin-skilled-openapi` SaaS-pull source
9
+ * (which handles signature verification, replay/SSRF guards, and last-good
10
+ * cache fallback).
11
+ */
12
+ import type { EdgeBundleCacheFactory, EdgeBundleCacheStore } from './kv-cache';
13
+ export interface ManagedEdgeOptions {
14
+ /** SaaS pull endpoint serving the signed bundle (HTTPS). */
15
+ endpoint: string;
16
+ /** Pinned JWT the SaaS issued for this server. */
17
+ authToken: string;
18
+ /** RFC 8707 resource indicator the JWT must encode (verified on pull/push). */
19
+ expectedAudience: string;
20
+ /** JWKS URL for verifying SaaS-issued tokens (HTTPS). */
21
+ jwksUrl: string;
22
+ /** Expected `iss` (issuer) claim. */
23
+ expectedIssuer: string;
24
+ /**
25
+ * Auto-update polling interval in ms (the boot pull is always immediate).
26
+ * Default 300000 (5 min) — the plugin re-pulls and hot-swaps on change.
27
+ *
28
+ * NOTE: IGNORED on the edge. `createEdgeMcp` disables the internal poll loop
29
+ * (Workers have no background execution between requests); a Cron Trigger
30
+ * drives {@link EdgeMcp.scheduled} → refresh instead. Set your refresh cadence
31
+ * via `[triggers] crontabs` in `wrangler.toml`, not this field.
32
+ */
33
+ pollIntervalMs?: number;
34
+ /** Also mount a push webhook (`POST /__skilled_openapi/push`) for synchronous updates. */
35
+ enableWebhook?: boolean;
36
+ /** Require a signed bundle. Default true. */
37
+ requireSignature?: boolean;
38
+ /** Trusted public keys for bundle signature verification. */
39
+ trustedKeys?: unknown[];
40
+ /** Dev mode — bypass signature checks + relax defaults (loud warning). Never in production. */
41
+ dev?: boolean;
42
+ /** Static credential map seeded into the executor's resolver (dev / single-tenant). */
43
+ credentials?: Record<string, string>;
44
+ /** Outbound HTTP / SSRF options for the executor. */
45
+ outbound?: Record<string, unknown>;
46
+ /**
47
+ * Cache dir for the last-good bundle (boot fallback when a fresh pull fails).
48
+ * Filesystem-only — a no-op on a Worker (there is no filesystem); use {@link
49
+ * ManagedEdgeOptions.cache} there instead.
50
+ */
51
+ bundleCacheDir?: string;
52
+ /**
53
+ * Pluggable last-good bundle cache, replacing the on-disk cache entirely.
54
+ *
55
+ * On Cloudflare Workers a KV binding lives on the per-request `env`, not in
56
+ * module scope, so prefer the `env`-resolving factory form — e.g.
57
+ * `cache: kvBundleCacheFromEnv('BUNDLE_CACHE')` (or a custom
58
+ * `(env) => createKvBundleCache(env.MY_KV)`). A plain store is also accepted
59
+ * for runtimes where the cache is available at construction time.
60
+ */
61
+ cache?: EdgeBundleCacheStore | EdgeBundleCacheFactory;
62
+ }
63
+ /**
64
+ * Map {@link ManagedEdgeOptions} → the options object that
65
+ * `@frontmcp/plugin-skilled-openapi`'s `init(...)` accepts (a `saas` bundle
66
+ * source + plugin flags). Pure + dependency-free so it's unit-testable without
67
+ * loading the plugin.
68
+ */
69
+ export declare function buildManagedOpenApiPluginOptions(managed: ManagedEdgeOptions): Record<string, unknown>;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@frontmcp/edge",
3
+ "version": "0.0.1",
4
+ "description": "Run a FrontMCP MCP server on Cloudflare Workers / V8 isolates from a plain config — no decorators, no build step",
5
+ "author": "AgentFront <info@agentfront.dev>",
6
+ "license": "Apache-2.0",
7
+ "keywords": [
8
+ "frontmcp",
9
+ "mcp",
10
+ "cloudflare",
11
+ "cloudflare-workers",
12
+ "workerd",
13
+ "edge",
14
+ "deno",
15
+ "bun",
16
+ "fetch",
17
+ "v8-isolate"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/agentfront/frontmcp.git",
22
+ "directory": "libs/edge"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/agentfront/frontmcp/issues"
26
+ },
27
+ "homepage": "https://github.com/agentfront/frontmcp/blob/main/libs/edge/README.md",
28
+ "type": "commonjs",
29
+ "main": "./index.js",
30
+ "module": "./esm/index.mjs",
31
+ "types": "./index.d.ts",
32
+ "sideEffects": false,
33
+ "exports": {
34
+ "./package.json": "./package.json",
35
+ ".": {
36
+ "require": {
37
+ "types": "./index.d.ts",
38
+ "default": "./index.js"
39
+ },
40
+ "import": {
41
+ "types": "./index.d.ts",
42
+ "default": "./esm/index.mjs"
43
+ }
44
+ },
45
+ "./esm": null
46
+ },
47
+ "engines": {
48
+ "node": ">=24.0.0"
49
+ },
50
+ "peerDependencies": {
51
+ "@frontmcp/plugin-skilled-openapi": "1.4.0",
52
+ "@frontmcp/sdk": "1.4.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@frontmcp/plugin-skilled-openapi": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^24.0.0",
61
+ "typescript": "^5.0.0"
62
+ }
63
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cloudflare **Durable Object** session host for stateful MCP on Workers.
3
+ *
4
+ * The stateless web path can't support the Streamable HTTP standalone GET
5
+ * notification stream: each request gets a fresh isolate/transport, so a
6
+ * `tools/call`'s notifications have no path back to the client's open GET
7
+ * stream. A Durable Object fixes this — one instance per `Mcp-Session-Id` holds
8
+ * a **persistent** `McpServer` + session-bound transport, so the GET stream
9
+ * stays open across requests and notifications reach it.
10
+ *
11
+ * It runs the SAME `http:request` flow as the stateless path (auth, quota,
12
+ * router, audit, metrics + hooks) — only the transport persists. The worker
13
+ * routes a request to its session DO; the DO runs the flow with its persistent
14
+ * transport threaded in.
15
+ */
16
+ import { runHttpRequestFlowWeb, type WebFetchSessionRouter, type WebStandardMcpPair } from '@frontmcp/sdk';
17
+ /** The FrontMCP scope type, derived to avoid widening the SDK's public surface. */
18
+ type Scope = Parameters<typeof runHttpRequestFlowWeb>[0];
19
+ /**
20
+ * Build the {@link WebFetchSessionRouter} that forwards an MCP request to its
21
+ * per-session Durable Object — addressed by `Mcp-Session-Id`, minting a fresh id
22
+ * for new sessions (`initialize`). Returns `undefined` (→ stateless fallback)
23
+ * when the DO binding isn't present or for CORS preflight (handled by the adapter).
24
+ */
25
+ export declare function createEdgeSessionRouter(bindingName: string): WebFetchSessionRouter;
26
+ /**
27
+ * Build the Durable Object class for stateful MCP sessions. `buildScope(env)`
28
+ * builds the FrontMCP scope inside the DO's isolate; `bridgeEnv(env)` mirrors the
29
+ * Worker `env` into `process.env` (so `session:verify` sees `MCP_SESSION_SECRET`,
30
+ * etc.). Each instance lazily builds its scope + a persistent transport once,
31
+ * then handles every request for its session on them.
32
+ */
33
+ export declare function createEdgeSessionDurableObject(buildScope: (env: unknown) => Promise<Scope>, bridgeEnv: (env: unknown) => void): {
34
+ new (_state: unknown, env: unknown): {
35
+ "__#private@#scopePromise"?: Promise<Scope>;
36
+ "__#private@#pair"?: WebStandardMcpPair;
37
+ readonly "__#private@#doEnv": unknown;
38
+ fetch(request: Request): Promise<Response>;
39
+ };
40
+ };
41
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * KV-backed cache for the skill SEARCH INDEX (the TF-IDF / BM25 "embedding"
3
+ * model + per-skill vectors), for fast cold starts on Cloudflare Workers.
4
+ *
5
+ * Building the index means tokenizing every skill, computing IDF across the
6
+ * corpus, and embedding each document — work a long-lived server pays once but a
7
+ * Worker would otherwise repeat on EVERY cold start. This adapter persists the
8
+ * built index snapshot to a Cloudflare **KV namespace** (keyed by a content hash
9
+ * of the indexed skills), so a cold start restores it instead of recomputing.
10
+ *
11
+ * ```ts
12
+ * import { createEdgeMcp } from '@frontmcp/edge';
13
+ *
14
+ * export default createEdgeMcp({
15
+ * info: { name: 'my-worker', version: '1.0.0' },
16
+ * apps: [MyApp],
17
+ * tasks: { enabled: false },
18
+ * skillIndex: { binding: 'FRONTMCP_SKILL_INDEX' }, // KV namespace binding
19
+ * });
20
+ * ```
21
+ */
22
+ import type { SkillIndexCache } from '@frontmcp/sdk';
23
+ import type { EdgeKvNamespace } from './kv-cache';
24
+ /** Tuning for {@link createKvSkillIndexCache}. */
25
+ export interface KvSkillIndexCacheOptions {
26
+ /** KV key prefix for snapshots. The content hash is appended. Default `frontmcp:skill-index:`. */
27
+ keyPrefix?: string;
28
+ /** Optional KV TTL (seconds). Omit to persist indefinitely. */
29
+ expirationTtl?: number;
30
+ }
31
+ /**
32
+ * Lazily resolve a skill-index cache from the per-request Worker `env`. CF
33
+ * bindings (KV namespaces) live on `env`, not module scope, so a cache that
34
+ * needs a binding must be built from `env` at request time.
35
+ */
36
+ export type EdgeSkillIndexCacheFactory = (env: unknown) => SkillIndexCache | undefined;
37
+ /**
38
+ * Build a {@link SkillIndexCache} backed by a Cloudflare KV namespace.
39
+ *
40
+ * Reads tolerate a missing key (cold KV) and corrupt JSON by resolving to
41
+ * `undefined` (a miss → rebuild). Writes are best-effort: a KV write failure is
42
+ * swallowed so a persist error never fails search.
43
+ */
44
+ export declare function createKvSkillIndexCache(kv: EdgeKvNamespace, options?: KvSkillIndexCacheOptions): SkillIndexCache;
45
+ /**
46
+ * Convenience {@link EdgeSkillIndexCacheFactory}: read a KV namespace from
47
+ * `env[binding]` at request time and wrap it. Returns `undefined` when the
48
+ * binding is absent (caching is simply skipped — search still works), rather
49
+ * than throwing, so a missing binding never bricks boot.
50
+ */
51
+ export declare function kvSkillIndexCacheFromEnv(binding: string, options?: KvSkillIndexCacheOptions): EdgeSkillIndexCacheFactory;