okengine 0.2.3 → 0.2.5
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/README.md +30 -4
- package/docs/spec/example.md +12 -3
- package/docs/spec/four-applications.md +10 -3
- package/package.json +9 -40
- package/src/cli/dev-app-runner.ts +45 -6
- package/src/cli/dev.ts +136 -17
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/load-config.images.test.ts +51 -0
- package/src/cli/load-config.ts +84 -6
- package/src/config/define-config.test.ts +61 -0
- package/src/config/index.ts +89 -6
- package/src/config/resolve-driver.test.ts +30 -0
- package/src/console/server/flows.ts +3 -1
- package/src/console/server/operator-db.test.ts +140 -2
- package/src/console/server/operator-db.ts +147 -1
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +8 -1
- package/src/console/server/state.ts +12 -1
- package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/App.tsx +10 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +29 -9
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +2 -0
- package/src/docker/index.ts +10 -0
- package/src/docker/stack-id.test.ts +86 -0
- package/src/docker/stack-id.ts +108 -0
- package/src/docker/stack.integration.test.ts +16 -7
- package/src/docker/types.ts +20 -1
- package/src/kernel/app.ts +39 -8
- package/src/kernel/boot-bind/store.test.ts +60 -0
- package/src/kernel/boot-bind/store.ts +142 -12
- package/src/kernel/boot.ts +28 -4
- package/src/mcp/server.ts +99 -57
- package/src/runtime/dev-request-log.test.ts +33 -0
- package/src/runtime/dev-request-log.ts +130 -0
- package/src/term.test.ts +98 -5
- package/src/term.ts +287 -6
package/src/kernel/app.ts
CHANGED
|
@@ -45,6 +45,11 @@ import {
|
|
|
45
45
|
type FxOperator,
|
|
46
46
|
type NamedRef,
|
|
47
47
|
} from "./fx.ts";
|
|
48
|
+
import {
|
|
49
|
+
currentDevSurface,
|
|
50
|
+
logDevRequest,
|
|
51
|
+
shouldLogDevRequests,
|
|
52
|
+
} from "../runtime/dev-request-log.ts";
|
|
48
53
|
import {
|
|
49
54
|
mergeHooks,
|
|
50
55
|
runPipeline,
|
|
@@ -126,6 +131,11 @@ export interface OkeOptions {
|
|
|
126
131
|
readonly archiveInputFields?: readonly string[];
|
|
127
132
|
/** Active environment for {@link OkeApp.boot} (defaults to `dev`). */
|
|
128
133
|
readonly env?: BootOptions["env"];
|
|
134
|
+
/**
|
|
135
|
+
* Local-server mode (`oke dev -s` / `OKE_STACK=1`) — force the `stack`
|
|
136
|
+
* driver profile at boot.
|
|
137
|
+
*/
|
|
138
|
+
readonly stack?: BootOptions["stack"];
|
|
129
139
|
/** Optional `oke.config.ts` document consumed at boot. */
|
|
130
140
|
readonly config?: BootOptions["config"];
|
|
131
141
|
/** Pre-built element runtimes (skip construction at boot when present). */
|
|
@@ -483,6 +493,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
483
493
|
bootEnv = overrides?.env ?? options.env ?? "dev";
|
|
484
494
|
const merged: BootOptions = {
|
|
485
495
|
env: bootEnv,
|
|
496
|
+
stack: overrides?.stack ?? options.stack,
|
|
486
497
|
config: overrides?.config ?? options.config,
|
|
487
498
|
elements: overrides?.elements ?? options.elements,
|
|
488
499
|
secrets: overrides?.secrets ?? options.secrets,
|
|
@@ -892,13 +903,31 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
892
903
|
},
|
|
893
904
|
execute,
|
|
894
905
|
async fetch(request) {
|
|
906
|
+
const started = performance.now();
|
|
907
|
+
let flowLabel: string | undefined;
|
|
895
908
|
const url = new URL(request.url);
|
|
896
909
|
const method = request.method.toUpperCase();
|
|
897
910
|
|
|
911
|
+
const respond = (response: Response): Response => {
|
|
912
|
+
if (shouldLogDevRequests()) {
|
|
913
|
+
logDevRequest({
|
|
914
|
+
surface: currentDevSurface(),
|
|
915
|
+
method,
|
|
916
|
+
path: url.pathname,
|
|
917
|
+
flow: flowLabel,
|
|
918
|
+
status: response.status,
|
|
919
|
+
ms: Math.round(performance.now() - started),
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return response;
|
|
923
|
+
};
|
|
924
|
+
|
|
898
925
|
if (method === "GET" && url.pathname === "/_oke/client.json") {
|
|
899
|
-
return
|
|
900
|
-
|
|
901
|
-
|
|
926
|
+
return respond(
|
|
927
|
+
new Response(JSON.stringify(routes), {
|
|
928
|
+
headers: { "content-type": "application/json" },
|
|
929
|
+
}),
|
|
930
|
+
);
|
|
902
931
|
}
|
|
903
932
|
|
|
904
933
|
if (method === "POST" && url.pathname.startsWith("/_oke/")) {
|
|
@@ -910,8 +939,9 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
910
939
|
const target =
|
|
911
940
|
flowsByName.get(`${unit}.${flowName}`) ?? flowsByName.get(flowName);
|
|
912
941
|
if (!target) {
|
|
913
|
-
return new Response("Not Found", { status: 404 });
|
|
942
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
914
943
|
}
|
|
944
|
+
flowLabel = target.name;
|
|
915
945
|
let internalInput: unknown;
|
|
916
946
|
try {
|
|
917
947
|
internalInput = await request.json();
|
|
@@ -924,15 +954,16 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
924
954
|
{ kind: "internal" } satisfies InternalTrigger,
|
|
925
955
|
{ request },
|
|
926
956
|
);
|
|
927
|
-
return encodeExecuteResult(internalResult);
|
|
957
|
+
return respond(encodeExecuteResult(internalResult));
|
|
928
958
|
}
|
|
929
959
|
}
|
|
930
960
|
|
|
931
961
|
const matched = router.match(method, url.pathname);
|
|
932
962
|
if (!matched) {
|
|
933
|
-
return new Response("Not Found", { status: 404 });
|
|
963
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
934
964
|
}
|
|
935
965
|
const { value: binding, params } = matched;
|
|
966
|
+
flowLabel = binding.flow.name;
|
|
936
967
|
|
|
937
968
|
let route = compiled.get(binding);
|
|
938
969
|
if (!route && binding.trigger.kind === "http") {
|
|
@@ -946,7 +977,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
946
977
|
if (route) {
|
|
947
978
|
const parsed = await route.parseValidate(request, params);
|
|
948
979
|
if (!parsed.ok) {
|
|
949
|
-
return encodeFailure(parsed.failure);
|
|
980
|
+
return respond(encodeFailure(parsed.failure));
|
|
950
981
|
}
|
|
951
982
|
input = parsed.input;
|
|
952
983
|
validated = true;
|
|
@@ -962,7 +993,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
962
993
|
{ request, params, validated },
|
|
963
994
|
);
|
|
964
995
|
|
|
965
|
-
return encodeExecuteResult(result);
|
|
996
|
+
return respond(encodeExecuteResult(result));
|
|
966
997
|
},
|
|
967
998
|
async dispatchSignal(signal, payload) {
|
|
968
999
|
const name = resolveName(signal);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store binder — stack profile (+ env overrides).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolveKvDriverId, resolveSqlDriverId } from "./store.ts";
|
|
7
|
+
|
|
8
|
+
describe("bindStore driver resolution", () => {
|
|
9
|
+
const prev = {
|
|
10
|
+
stack: process.env.OKE_STACK,
|
|
11
|
+
sql: process.env.OKE_SQL_DRIVER,
|
|
12
|
+
kv: process.env.OKE_KV_DRIVER,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (prev.stack === undefined) delete process.env.OKE_STACK;
|
|
17
|
+
else process.env.OKE_STACK = prev.stack;
|
|
18
|
+
if (prev.sql === undefined) delete process.env.OKE_SQL_DRIVER;
|
|
19
|
+
else process.env.OKE_SQL_DRIVER = prev.sql;
|
|
20
|
+
if (prev.kv === undefined) delete process.env.OKE_KV_DRIVER;
|
|
21
|
+
else process.env.OKE_KV_DRIVER = prev.kv;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("dev env keeps sqlite / memory from config", () => {
|
|
25
|
+
const options = {
|
|
26
|
+
config: {
|
|
27
|
+
drivers: {
|
|
28
|
+
store: {
|
|
29
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
30
|
+
kv: { dev: "memory", stack: "redis", prod: "redis" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
expect(resolveSqlDriverId(options, "dev", false)).toBe("sqlite");
|
|
36
|
+
expect(resolveKvDriverId(options, "dev", false)).toBe("memory");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("stack env uses stack profile (falls back to prod)", () => {
|
|
40
|
+
const options = {
|
|
41
|
+
config: {
|
|
42
|
+
drivers: {
|
|
43
|
+
store: {
|
|
44
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
45
|
+
kv: { dev: "memory", prod: "redis" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
expect(resolveSqlDriverId(options, "stack", true)).toBe("postgres");
|
|
51
|
+
expect(resolveKvDriverId(options, "stack", true)).toBe("redis");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("stack mode honours OKE_*_DRIVER overrides", () => {
|
|
55
|
+
process.env.OKE_SQL_DRIVER = "postgres";
|
|
56
|
+
process.env.OKE_KV_DRIVER = "redis";
|
|
57
|
+
expect(resolveSqlDriverId({}, "stack", true)).toBe("postgres");
|
|
58
|
+
expect(resolveKvDriverId({}, "stack", true)).toBe("redis");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -2,42 +2,70 @@
|
|
|
2
2
|
* Lazy store binder — loaded only when Store is declared.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
resolveDriverId,
|
|
7
|
+
type ConfigEnv,
|
|
8
|
+
} from "../../config/index.ts";
|
|
5
9
|
import { memoryDrivers } from "../../drivers/memory.ts";
|
|
10
|
+
import { postgresDriver } from "../../drivers/postgres.ts";
|
|
11
|
+
import { redisDriver } from "../../drivers/redis.ts";
|
|
12
|
+
import { sqliteDriver } from "../../drivers/sqlite.ts";
|
|
13
|
+
import type { KvDriver, SqlDriver } from "../../drivers/types.ts";
|
|
6
14
|
import {
|
|
7
15
|
createStoreRuntime,
|
|
8
16
|
type StoreRuntime,
|
|
9
17
|
} from "../../elements/store.ts";
|
|
10
|
-
import {
|
|
11
|
-
resolveDriverId,
|
|
12
|
-
type ConfigEnv,
|
|
13
|
-
} from "../../config/index.ts";
|
|
18
|
+
import type { StoreDecl } from "../../elements/store/declare.ts";
|
|
14
19
|
import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Construct a Store runtime and register facet declarations.
|
|
18
23
|
*
|
|
24
|
+
* In stack mode (`env === "stack"` / `OKE_STACK=1`), SQL/KV resolve from the
|
|
25
|
+
* `stack` driver map (falling back to `prod`) and URLs from `.env.stack`.
|
|
26
|
+
*
|
|
19
27
|
* @param options - Boot options
|
|
20
28
|
* @param env - Active environment
|
|
21
29
|
* @param now - Clock
|
|
30
|
+
* @param stack - Prefer compose URLs when opening postgres/redis
|
|
22
31
|
*/
|
|
23
32
|
export function bindStore(
|
|
24
33
|
options: BootOptions,
|
|
25
34
|
env: ConfigEnv,
|
|
26
35
|
now: () => number,
|
|
36
|
+
stack = false,
|
|
27
37
|
): StoreRuntime {
|
|
28
|
-
const sqlId =
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const sqlId = resolveSqlDriverId(options, env, stack);
|
|
39
|
+
const kvId = resolveKvDriverId(options, env, stack);
|
|
40
|
+
const sqlUrl = sqlUrlFor(sqlId, stack);
|
|
41
|
+
const kvUrl = kvUrlFor(kvId, stack);
|
|
42
|
+
|
|
43
|
+
const sqlBindings: Record<
|
|
44
|
+
string,
|
|
45
|
+
{ name: string; primary: { url: string } }
|
|
46
|
+
> = {};
|
|
47
|
+
const kvBindings: Record<string, { url?: string }> = {};
|
|
48
|
+
|
|
49
|
+
for (const decl of options.stores ?? []) {
|
|
50
|
+
if (isSqlDecl(decl)) {
|
|
51
|
+
sqlBindings[decl.name] = {
|
|
52
|
+
name: decl.name,
|
|
53
|
+
primary: { url: sqlUrl },
|
|
54
|
+
};
|
|
55
|
+
} else if (isKvDecl(decl)) {
|
|
56
|
+
kvBindings[decl.name] = kvUrl !== undefined ? { url: kvUrl } : {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
const store = createStoreRuntime({
|
|
35
61
|
drivers: {
|
|
36
|
-
sql:
|
|
37
|
-
kv:
|
|
62
|
+
sql: sqlDriverFor(sqlId),
|
|
63
|
+
kv: kvDriverFor(kvId),
|
|
38
64
|
files: memoryDrivers.files,
|
|
39
65
|
index: memoryDrivers.index,
|
|
40
66
|
},
|
|
67
|
+
sql: sqlBindings,
|
|
68
|
+
kv: kvBindings,
|
|
41
69
|
now,
|
|
42
70
|
});
|
|
43
71
|
for (const decl of options.stores ?? []) {
|
|
@@ -45,3 +73,105 @@ export function bindStore(
|
|
|
45
73
|
}
|
|
46
74
|
return store;
|
|
47
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param options - Boot options
|
|
79
|
+
* @param env - Active env
|
|
80
|
+
* @param stack - Stack mode
|
|
81
|
+
*/
|
|
82
|
+
export function resolveSqlDriverId(
|
|
83
|
+
options: BootOptions,
|
|
84
|
+
env: ConfigEnv,
|
|
85
|
+
stack: boolean,
|
|
86
|
+
): string {
|
|
87
|
+
const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
|
|
88
|
+
if (stack && fromEnv) return fromEnv;
|
|
89
|
+
const resolved = resolveDriverId(options.config?.drivers?.store?.sql, env);
|
|
90
|
+
if (resolved) return resolved;
|
|
91
|
+
return stack ? "postgres" : "memory";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param options - Boot options
|
|
96
|
+
* @param env - Active env
|
|
97
|
+
* @param stack - Stack mode
|
|
98
|
+
*/
|
|
99
|
+
export function resolveKvDriverId(
|
|
100
|
+
options: BootOptions,
|
|
101
|
+
env: ConfigEnv,
|
|
102
|
+
stack: boolean,
|
|
103
|
+
): string {
|
|
104
|
+
const fromEnv = process.env.OKE_KV_DRIVER?.trim();
|
|
105
|
+
if (stack && fromEnv) return fromEnv;
|
|
106
|
+
const resolved = resolveDriverId(options.config?.drivers?.store?.kv, env);
|
|
107
|
+
if (resolved) return resolved;
|
|
108
|
+
return stack ? "redis" : "memory";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sqlDriverFor(id: string): SqlDriver {
|
|
112
|
+
switch (id) {
|
|
113
|
+
case "postgres":
|
|
114
|
+
return postgresDriver;
|
|
115
|
+
case "sqlite":
|
|
116
|
+
return sqliteDriver;
|
|
117
|
+
case "memory":
|
|
118
|
+
return memoryDrivers.sql;
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`oke boot: unknown sql driver "${id}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function kvDriverFor(id: string): KvDriver {
|
|
125
|
+
switch (id) {
|
|
126
|
+
case "redis":
|
|
127
|
+
return redisDriver;
|
|
128
|
+
case "memory":
|
|
129
|
+
return memoryDrivers.kv;
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`oke boot: unknown kv driver "${id}"`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sqlUrlFor(sqlId: string, stack: boolean): string {
|
|
136
|
+
if (sqlId === "postgres") {
|
|
137
|
+
const url =
|
|
138
|
+
process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
|
|
139
|
+
if (!url) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
stack
|
|
142
|
+
? "oke boot: postgres driver needs DATABASE_URL (did `oke dev -s` write .env.stack?)"
|
|
143
|
+
: "oke boot: postgres driver needs DATABASE_URL",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
if (sqlId === "sqlite") {
|
|
149
|
+
return process.env.OKE_SQLITE_URL ?? ".oke/app.sqlite";
|
|
150
|
+
}
|
|
151
|
+
return ":memory:";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function kvUrlFor(kvId: string, stack: boolean): string | undefined {
|
|
155
|
+
if (kvId !== "redis") return undefined;
|
|
156
|
+
const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
|
|
157
|
+
if (!url) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
stack
|
|
160
|
+
? "oke boot: redis driver needs REDIS_URL (did `oke dev -s` write .env.stack?)"
|
|
161
|
+
: "oke boot: redis driver needs REDIS_URL",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isSqlDecl(
|
|
168
|
+
decl: StoreDecl,
|
|
169
|
+
): decl is Extract<StoreDecl, { facet: "sql" }> {
|
|
170
|
+
return decl.facet === "sql";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isKvDecl(
|
|
174
|
+
decl: StoreDecl,
|
|
175
|
+
): decl is Extract<StoreDecl, { facet: "kv" }> {
|
|
176
|
+
return decl.facet === "kv";
|
|
177
|
+
}
|
package/src/kernel/boot.ts
CHANGED
|
@@ -76,8 +76,17 @@ export interface ElementRuntimes {
|
|
|
76
76
|
|
|
77
77
|
/** Declarations + options consumed by {@link bootApplication}. */
|
|
78
78
|
export interface BootOptions {
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Active environment (defaults to `dev`, or `stack` when
|
|
81
|
+
* {@link stack} / `OKE_STACK=1`).
|
|
82
|
+
*/
|
|
80
83
|
readonly env?: ConfigEnv;
|
|
84
|
+
/**
|
|
85
|
+
* Local-server mode (`oke dev -s` / `OKE_STACK=1`): force driver maps to the
|
|
86
|
+
* `stack` profile and prefer compose URLs (`.env.stack`).
|
|
87
|
+
* When unset, derived from `process.env.OKE_STACK`.
|
|
88
|
+
*/
|
|
89
|
+
readonly stack?: boolean;
|
|
81
90
|
/** Optional `oke.config.ts` document. */
|
|
82
91
|
readonly config?: OkeConfig;
|
|
83
92
|
/** Pre-built runtimes (skip construction when present). */
|
|
@@ -243,9 +252,24 @@ async function loadBind<T>(name: string): Promise<T> {
|
|
|
243
252
|
* @param options - Declarations, config, pre-built runtimes
|
|
244
253
|
*/
|
|
245
254
|
export async function bootApplication(
|
|
246
|
-
|
|
255
|
+
input: BootOptions = {},
|
|
247
256
|
): Promise<BootResult> {
|
|
248
|
-
const
|
|
257
|
+
const stack =
|
|
258
|
+
input.stack === true ||
|
|
259
|
+
(input.stack !== false && process.env.OKE_STACK === "1");
|
|
260
|
+
// `-s` always selects the `stack` driver profile — not a mix of test/dev +
|
|
261
|
+
// prod store overrides (templates often pin `env: "test"` for harnesses).
|
|
262
|
+
const env: ConfigEnv = stack ? "stack" : (input.env ?? "dev");
|
|
263
|
+
let config = input.config;
|
|
264
|
+
if (config === undefined) {
|
|
265
|
+
try {
|
|
266
|
+
const { loadOkeConfig } = await import("../cli/load-config.ts");
|
|
267
|
+
config = (await loadOkeConfig(process.cwd())).config;
|
|
268
|
+
} catch {
|
|
269
|
+
config = undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const options: BootOptions = { ...input, config, env, stack };
|
|
249
273
|
const pre = options.elements ?? {};
|
|
250
274
|
const now = options.now ?? (() => Date.now());
|
|
251
275
|
const needs = resolveElementNeeds(options);
|
|
@@ -340,7 +364,7 @@ export async function bootApplication(
|
|
|
340
364
|
let store = pre.store;
|
|
341
365
|
if (needs.store) {
|
|
342
366
|
if (!store) {
|
|
343
|
-
store = storeBind!.bindStore(options, env, now);
|
|
367
|
+
store = storeBind!.bindStore(options, env, now, stack);
|
|
344
368
|
} else {
|
|
345
369
|
for (const decl of options.stores ?? []) {
|
|
346
370
|
store.register?.(decl);
|
package/src/mcp/server.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* adapters receive structured operator ids.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
logDevRequest,
|
|
13
|
+
shouldLogDevRequests,
|
|
14
|
+
} from "../runtime/dev-request-log.ts";
|
|
11
15
|
import {
|
|
12
16
|
checkRequestSecurity,
|
|
13
17
|
forbiddenResponse,
|
|
@@ -67,28 +71,48 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
67
71
|
const transportSessions = new Set<string>();
|
|
68
72
|
|
|
69
73
|
const fetch = async (request: Request): Promise<Response> => {
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
let flowLabel: string | undefined;
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
const method = request.method.toUpperCase();
|
|
78
|
+
|
|
79
|
+
const respond = (response: Response): Response => {
|
|
80
|
+
if (shouldLogDevRequests()) {
|
|
81
|
+
logDevRequest({
|
|
82
|
+
surface: "MCP",
|
|
83
|
+
method,
|
|
84
|
+
path: url.pathname,
|
|
85
|
+
flow: flowLabel,
|
|
86
|
+
status: response.status,
|
|
87
|
+
ms: Math.round(performance.now() - started),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
91
|
+
};
|
|
92
|
+
|
|
70
93
|
const security = checkRequestSecurity(request, allowed);
|
|
71
|
-
if (!security.ok) return forbiddenResponse(security.reason);
|
|
94
|
+
if (!security.ok) return respond(forbiddenResponse(security.reason));
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return Response.json({ ok: true, surface: "mcp" });
|
|
96
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
97
|
+
return respond(Response.json({ ok: true, surface: "mcp" }));
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
if (
|
|
79
|
-
return new Response("Not Found", { status: 404 });
|
|
100
|
+
if (method !== "POST" || url.pathname !== "/mcp") {
|
|
101
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
const bearer = extractBearer(request.headers.get("authorization"));
|
|
83
105
|
if (bearer === null) {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
return respond(
|
|
107
|
+
jsonRpcHttp(
|
|
108
|
+
rpcError(
|
|
109
|
+
null,
|
|
110
|
+
RpcErrorCode.unauthorized,
|
|
111
|
+
"Bearer token required",
|
|
112
|
+
asData({ reason: "missing-token" }, "error"),
|
|
113
|
+
),
|
|
114
|
+
401,
|
|
90
115
|
),
|
|
91
|
-
401,
|
|
92
116
|
);
|
|
93
117
|
}
|
|
94
118
|
|
|
@@ -103,14 +127,16 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
103
127
|
} catch (err) {
|
|
104
128
|
const message =
|
|
105
129
|
err instanceof SessionError ? err.message : "authentication failed";
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
return respond(
|
|
131
|
+
jsonRpcHttp(
|
|
132
|
+
rpcError(
|
|
133
|
+
null,
|
|
134
|
+
RpcErrorCode.unauthorized,
|
|
135
|
+
message,
|
|
136
|
+
asData({ reason: "auth-failed", message }, "error"),
|
|
137
|
+
),
|
|
138
|
+
401,
|
|
112
139
|
),
|
|
113
|
-
401,
|
|
114
140
|
);
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -122,22 +148,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
122
148
|
try {
|
|
123
149
|
body = await request.json();
|
|
124
150
|
} catch {
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
return respond(
|
|
152
|
+
jsonRpcHttp(
|
|
153
|
+
rpcError(null, RpcErrorCode.parse, "invalid JSON body"),
|
|
154
|
+
400,
|
|
155
|
+
),
|
|
128
156
|
);
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
const parsed = parseJsonRpcRequest(body);
|
|
132
160
|
if (!parsed.ok) {
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
return respond(
|
|
162
|
+
jsonRpcHttp(
|
|
163
|
+
rpcError(null, RpcErrorCode.invalidRequest, parsed.message),
|
|
164
|
+
400,
|
|
165
|
+
),
|
|
136
166
|
);
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
const { request: rpc } = parsed;
|
|
140
170
|
const id: JsonRpcId = rpc.id;
|
|
171
|
+
flowLabel = rpc.method;
|
|
141
172
|
|
|
142
173
|
switch (rpc.method) {
|
|
143
174
|
case "initialize": {
|
|
@@ -149,10 +180,10 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
149
180
|
serverInfo: { name: "okengine-mcp", version },
|
|
150
181
|
sessionId,
|
|
151
182
|
};
|
|
152
|
-
return jsonRpcHttp(rpcSuccess(id, result));
|
|
183
|
+
return respond(jsonRpcHttp(rpcSuccess(id, result)));
|
|
153
184
|
}
|
|
154
185
|
case "ping":
|
|
155
|
-
return jsonRpcHttp(rpcSuccess(id, { ok: true }));
|
|
186
|
+
return respond(jsonRpcHttp(rpcSuccess(id, { ok: true })));
|
|
156
187
|
case "tools/list": {
|
|
157
188
|
const listed = tools.listTools().map((t) => ({
|
|
158
189
|
name: t.name,
|
|
@@ -163,22 +194,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
163
194
|
destructiveHint: t.mutability === "write",
|
|
164
195
|
},
|
|
165
196
|
}));
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
return respond(
|
|
198
|
+
jsonRpcHttp(
|
|
199
|
+
rpcSuccess(id, {
|
|
200
|
+
tools: listed,
|
|
201
|
+
// Catalogue itself is data.
|
|
202
|
+
_oke: asData({ count: listed.length }, "catalog"),
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
172
205
|
);
|
|
173
206
|
}
|
|
174
207
|
case "tools/call": {
|
|
175
208
|
const call = parseToolsCallParams(rpc.params);
|
|
176
209
|
if (!call.ok) {
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
return respond(
|
|
211
|
+
jsonRpcHttp(
|
|
212
|
+
rpcError(id, RpcErrorCode.invalidParams, call.message),
|
|
213
|
+
400,
|
|
214
|
+
),
|
|
180
215
|
);
|
|
181
216
|
}
|
|
217
|
+
flowLabel = call.name;
|
|
182
218
|
const result = await tools.callTool(
|
|
183
219
|
requester,
|
|
184
220
|
call.name,
|
|
@@ -193,34 +229,40 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
193
229
|
: result.code === "not-found"
|
|
194
230
|
? RpcErrorCode.methodNotFound
|
|
195
231
|
: RpcErrorCode.invalidParams;
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
return respond(
|
|
233
|
+
jsonRpcHttp(
|
|
234
|
+
rpcError(id, code, result.message, result.data),
|
|
235
|
+
result.code === "unauthorized" ? 401 : 403,
|
|
236
|
+
),
|
|
199
237
|
);
|
|
200
238
|
}
|
|
201
239
|
// MCP content blocks: text carries JSON of the inert envelope.
|
|
202
240
|
// Agents must treat it as data (envelope.kind === "data").
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
return respond(
|
|
242
|
+
jsonRpcHttp(
|
|
243
|
+
rpcSuccess(id, {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: JSON.stringify(result.data),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
structuredContent: result.data,
|
|
251
|
+
isError: false,
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
214
254
|
);
|
|
215
255
|
}
|
|
216
256
|
default:
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
257
|
+
return respond(
|
|
258
|
+
jsonRpcHttp(
|
|
259
|
+
rpcError(
|
|
260
|
+
id,
|
|
261
|
+
RpcErrorCode.methodNotFound,
|
|
262
|
+
`method not found: ${rpc.method}`,
|
|
263
|
+
),
|
|
264
|
+
404,
|
|
222
265
|
),
|
|
223
|
-
404,
|
|
224
266
|
);
|
|
225
267
|
}
|
|
226
268
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev request log gating and silence rules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
isSilentDevRequest,
|
|
8
|
+
shouldLogDevRequests,
|
|
9
|
+
} from "./dev-request-log.ts";
|
|
10
|
+
|
|
11
|
+
describe("dev-request-log", () => {
|
|
12
|
+
const prev = process.env.OKE_DEV_REQUEST_LOG;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (prev === undefined) delete process.env.OKE_DEV_REQUEST_LOG;
|
|
16
|
+
else process.env.OKE_DEV_REQUEST_LOG = prev;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("shouldLogDevRequests follows OKE_DEV_REQUEST_LOG", () => {
|
|
20
|
+
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
21
|
+
expect(shouldLogDevRequests()).toBe(true);
|
|
22
|
+
process.env.OKE_DEV_REQUEST_LOG = "0";
|
|
23
|
+
expect(shouldLogDevRequests()).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("isSilentDevRequest skips live, health, assets, client.json", () => {
|
|
27
|
+
expect(isSilentDevRequest("GET", "/console/live")).toBe(true);
|
|
28
|
+
expect(isSilentDevRequest("GET", "/health")).toBe(true);
|
|
29
|
+
expect(isSilentDevRequest("GET", "/assets/index.js")).toBe(true);
|
|
30
|
+
expect(isSilentDevRequest("GET", "/_oke/client.json")).toBe(true);
|
|
31
|
+
expect(isSilentDevRequest("POST", "/console/flows")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|