@decocms/runtime 0.0.1-testing-beta.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.
Files changed (85) hide show
  1. package/config-schema.json +553 -0
  2. package/dist/admin.d.ts +5 -0
  3. package/dist/admin.js +21 -0
  4. package/dist/admin.js.map +1 -0
  5. package/dist/bindings/deconfig/index.d.ts +9 -0
  6. package/dist/bindings/deconfig/index.js +9 -0
  7. package/dist/bindings/deconfig/index.js.map +1 -0
  8. package/dist/bindings/index.d.ts +1053 -0
  9. package/dist/bindings/index.js +132 -0
  10. package/dist/bindings/index.js.map +1 -0
  11. package/dist/chunk-4XSQKJLU.js +105 -0
  12. package/dist/chunk-4XSQKJLU.js.map +1 -0
  13. package/dist/chunk-AOFOWQXY.js +27 -0
  14. package/dist/chunk-AOFOWQXY.js.map +1 -0
  15. package/dist/chunk-F6XZPFWM.js +127 -0
  16. package/dist/chunk-F6XZPFWM.js.map +1 -0
  17. package/dist/chunk-IB3KGSMB.js +150 -0
  18. package/dist/chunk-IB3KGSMB.js.map +1 -0
  19. package/dist/chunk-NKUMVYKI.js +128 -0
  20. package/dist/chunk-NKUMVYKI.js.map +1 -0
  21. package/dist/chunk-NMXOC7PT.js +763 -0
  22. package/dist/chunk-NMXOC7PT.js.map +1 -0
  23. package/dist/chunk-OSSKGDAG.js +395 -0
  24. package/dist/chunk-OSSKGDAG.js.map +1 -0
  25. package/dist/chunk-UHR3BLMF.js +92 -0
  26. package/dist/chunk-UHR3BLMF.js.map +1 -0
  27. package/dist/client.d.ts +28 -0
  28. package/dist/client.js +4 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/connection-DDtQYrea.d.ts +30 -0
  31. package/dist/drizzle.d.ts +47 -0
  32. package/dist/drizzle.js +121 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/index-AKVjfH4b.d.ts +336 -0
  35. package/dist/index-kMsI0ELb.d.ts +530 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.js +507 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mastra.d.ts +8 -0
  40. package/dist/mastra.js +5 -0
  41. package/dist/mastra.js.map +1 -0
  42. package/dist/mcp-Bv7IAgWX.d.ts +109 -0
  43. package/dist/mcp-client.d.ts +236 -0
  44. package/dist/mcp-client.js +3 -0
  45. package/dist/mcp-client.js.map +1 -0
  46. package/dist/proxy.d.ts +10 -0
  47. package/dist/proxy.js +4 -0
  48. package/dist/proxy.js.map +1 -0
  49. package/dist/resources.d.ts +362 -0
  50. package/dist/resources.js +3 -0
  51. package/dist/resources.js.map +1 -0
  52. package/dist/views.d.ts +72 -0
  53. package/dist/views.js +3 -0
  54. package/dist/views.js.map +1 -0
  55. package/package.json +98 -0
  56. package/src/admin.ts +16 -0
  57. package/src/auth.ts +233 -0
  58. package/src/bindings/README.md +132 -0
  59. package/src/bindings/binder.ts +143 -0
  60. package/src/bindings/channels.ts +54 -0
  61. package/src/bindings/deconfig/helpers.ts +107 -0
  62. package/src/bindings/deconfig/index.ts +1 -0
  63. package/src/bindings/deconfig/resources.ts +659 -0
  64. package/src/bindings/deconfig/types.ts +106 -0
  65. package/src/bindings/index.ts +61 -0
  66. package/src/bindings/resources/bindings.ts +99 -0
  67. package/src/bindings/resources/helpers.ts +95 -0
  68. package/src/bindings/resources/schemas.ts +265 -0
  69. package/src/bindings/utils.ts +22 -0
  70. package/src/bindings/views.ts +14 -0
  71. package/src/bindings.ts +179 -0
  72. package/src/client.ts +201 -0
  73. package/src/connection.ts +53 -0
  74. package/src/drizzle.ts +201 -0
  75. package/src/http-client-transport.ts +66 -0
  76. package/src/index.ts +394 -0
  77. package/src/mastra.ts +666 -0
  78. package/src/mcp-client.ts +119 -0
  79. package/src/mcp.ts +171 -0
  80. package/src/proxy.ts +204 -0
  81. package/src/resources.ts +168 -0
  82. package/src/state.ts +44 -0
  83. package/src/views.ts +26 -0
  84. package/src/well-known.ts +20 -0
  85. package/src/wrangler.ts +146 -0
@@ -0,0 +1,179 @@
1
+ import type { MCPConnection } from "./connection.ts";
2
+ import type { DefaultEnv, RequestContext } from "./index.ts";
3
+ import { MCPClient } from "./mcp.ts";
4
+ import type {
5
+ BindingBase,
6
+ ContractBinding,
7
+ MCPBinding,
8
+ MCPIntegrationNameBinding,
9
+ } from "./wrangler.ts";
10
+
11
+ interface IntegrationContext {
12
+ integrationId: string;
13
+ workspace: string;
14
+ branch?: string;
15
+ decoCmsApiUrl?: string;
16
+ }
17
+
18
+ const normalizeWorkspace = (workspace: string) => {
19
+ if (workspace.startsWith("/users")) {
20
+ return workspace;
21
+ }
22
+ if (workspace.startsWith("/shared")) {
23
+ return workspace;
24
+ }
25
+ if (workspace.includes("/")) {
26
+ return workspace;
27
+ }
28
+ return `/shared/${workspace}`;
29
+ };
30
+
31
+ /**
32
+ * Url: /apps/mcp?appName=$appName
33
+ */
34
+ const createAppsUrl = ({
35
+ appName,
36
+ decoChatApiUrl,
37
+ }: {
38
+ appName: string;
39
+ decoChatApiUrl?: string;
40
+ }) =>
41
+ new URL(
42
+ `/apps/mcp?appName=${appName}`,
43
+ decoChatApiUrl ?? "https://api.decocms.com",
44
+ ).href;
45
+ /**
46
+ * Url: /:workspace.root/:workspace.slug/:integrationId/mcp
47
+ */
48
+ const createIntegrationsUrl = ({
49
+ integrationId,
50
+ workspace,
51
+ decoCmsApiUrl,
52
+ branch,
53
+ }: IntegrationContext) => {
54
+ const base = `${normalizeWorkspace(workspace)}/${integrationId}/mcp`;
55
+ const url = new URL(base, decoCmsApiUrl ?? "https://api.decocms.com");
56
+ branch && url.searchParams.set("branch", branch);
57
+ return url.href;
58
+ };
59
+
60
+ type WorkspaceClientContext = Omit<
61
+ RequestContext,
62
+ "ensureAuthenticated" | "state" | "fetchIntegrationMetadata"
63
+ >;
64
+ export const workspaceClient = (
65
+ ctx: WorkspaceClientContext,
66
+ decocmsApiUrl?: string,
67
+ ): ReturnType<(typeof MCPClient)["forWorkspace"]> => {
68
+ return MCPClient.forWorkspace(ctx.workspace, ctx.token, decocmsApiUrl);
69
+ };
70
+
71
+ const mcpClientForAppName = (appName: string, decoChatApiUrl?: string) => {
72
+ const mcpConnection: MCPConnection = {
73
+ type: "HTTP",
74
+ url: createAppsUrl({
75
+ appName,
76
+ decoChatApiUrl,
77
+ }),
78
+ };
79
+
80
+ return MCPClient.forConnection(mcpConnection, decoChatApiUrl);
81
+ };
82
+
83
+ export const proxyConnectionForId = (
84
+ integrationId: string,
85
+ ctx: Omit<WorkspaceClientContext, "token"> & {
86
+ token?: string;
87
+ cookie?: string;
88
+ },
89
+ decocmsApiUrl?: string,
90
+ appName?: string,
91
+ ): MCPConnection => {
92
+ let headers: Record<string, string> | undefined = appName
93
+ ? { "x-caller-app": appName }
94
+ : undefined;
95
+ if (ctx.cookie) {
96
+ headers ??= {};
97
+ headers.cookie = ctx.cookie;
98
+ }
99
+ return {
100
+ type: "HTTP",
101
+ url: createIntegrationsUrl({
102
+ integrationId,
103
+ workspace: ctx.workspace,
104
+ decoCmsApiUrl: decocmsApiUrl,
105
+ branch: ctx.branch,
106
+ }),
107
+ token: ctx.token,
108
+ headers,
109
+ };
110
+ };
111
+ const mcpClientForIntegrationId = (
112
+ integrationId: string,
113
+ ctx: WorkspaceClientContext,
114
+ decocmsApiUrl?: string,
115
+ appName?: string,
116
+ ) => {
117
+ const mcpConnection = proxyConnectionForId(
118
+ integrationId,
119
+ ctx,
120
+ decocmsApiUrl,
121
+ appName,
122
+ );
123
+
124
+ // TODO(@igorbrasileiro): Switch this proxy to be a proxy that call MCP Client.toolCall from @modelcontextprotocol
125
+ return MCPClient.forConnection(mcpConnection, decocmsApiUrl);
126
+ };
127
+
128
+ function mcpClientFromState(
129
+ binding: BindingBase | MCPIntegrationNameBinding,
130
+ env: DefaultEnv,
131
+ ) {
132
+ const ctx = env.DECO_REQUEST_CONTEXT;
133
+ const bindingFromState = ctx?.state?.[binding.name];
134
+ const integrationId =
135
+ bindingFromState &&
136
+ typeof bindingFromState === "object" &&
137
+ "value" in bindingFromState
138
+ ? bindingFromState.value
139
+ : undefined;
140
+ if (typeof integrationId !== "string" && "integration_name" in binding) {
141
+ // in case of a binding to an app name, we need to use the new apps/mcp endpoint which will proxy the request to the app but without any token
142
+ return mcpClientForAppName(binding.integration_name, env.DECO_API_URL);
143
+ }
144
+ return mcpClientForIntegrationId(
145
+ integrationId,
146
+ ctx,
147
+ env.DECO_API_URL,
148
+ env.DECO_APP_NAME,
149
+ );
150
+ }
151
+
152
+ export const createContractBinding = (
153
+ binding: ContractBinding,
154
+ env: DefaultEnv,
155
+ ) => {
156
+ return mcpClientFromState(binding, env);
157
+ };
158
+
159
+ export const createIntegrationBinding = (
160
+ binding: MCPBinding,
161
+ env: DefaultEnv,
162
+ ) => {
163
+ const integrationId =
164
+ "integration_id" in binding ? binding.integration_id : undefined;
165
+ if (!integrationId) {
166
+ return mcpClientFromState(binding, env);
167
+ }
168
+ // bindings pointed to an specific integration id are binded using the app deployment workspace
169
+ return mcpClientForIntegrationId(
170
+ integrationId,
171
+ {
172
+ workspace: env.DECO_WORKSPACE,
173
+ token: env.DECO_API_TOKEN,
174
+ branch: env.DECO_REQUEST_CONTEXT?.branch,
175
+ },
176
+ env.DECO_API_URL,
177
+ env.DECO_APP_NAME,
178
+ );
179
+ };
package/src/client.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { toAsyncIterator } from "./bindings/deconfig/helpers.ts";
2
+ // Extract resource name from DECO_RESOURCE_${NAME}_READ pattern
3
+ type ExtractResourceName<K> = K extends `DECO_RESOURCE_${infer Name}_READ`
4
+ ? Name
5
+ : never;
6
+
7
+ // Generate SUBSCRIBE method name from resource name
8
+ type SubscribeMethodName<Name extends string> =
9
+ `DECO_RESOURCE_${Name}_SUBSCRIBE`;
10
+
11
+ // Extract data type from READ method return type
12
+ type ExtractReadData<T> = T extends Promise<{ data: infer D }>
13
+ ? D
14
+ : T extends { data: infer D }
15
+ ? D
16
+ : never;
17
+
18
+ // Generate all SUBSCRIBE method names for a given type
19
+ type SubscribeMethods<T> = {
20
+ [K in keyof T as K extends `DECO_RESOURCE_${string}_READ`
21
+ ? SubscribeMethodName<ExtractResourceName<K>>
22
+ : never]: K extends `DECO_RESOURCE_${string}_READ`
23
+ ? // oxlint-disable-next-line no-explicit-any
24
+ T[K] extends (...args: any) => any
25
+ ? (args: { id: string } | { uri: string }) => AsyncIterableIterator<{
26
+ uri: string;
27
+ data: ExtractReadData<Awaited<ReturnType<T[K]>>>;
28
+ }>
29
+ : never
30
+ : never;
31
+ };
32
+
33
+ export type MCPClient<T> = {
34
+ // oxlint-disable-next-line no-explicit-any
35
+ [K in keyof T]: T[K] extends (...args: any) => any
36
+ ? (
37
+ args: Parameters<T[K]>[0],
38
+ init?: CustomInit,
39
+ ) => Promise<Awaited<ReturnType<T[K]>>>
40
+ : never;
41
+ } & SubscribeMethods<T>;
42
+
43
+ export type CustomInit = RequestInit & {
44
+ handleResponse?: (response: Response) => Promise<unknown>;
45
+ };
46
+
47
+ export const DECO_MCP_CLIENT_HEADER = "X-Deco-MCP-Client";
48
+
49
+ export const DEFAULT_INIT: CustomInit = {
50
+ credentials: "include",
51
+ headers: {
52
+ [DECO_MCP_CLIENT_HEADER]: "true",
53
+ },
54
+ };
55
+
56
+ /**
57
+ * Helper function to call an MCP tool via fetch
58
+ */
59
+ async function callMCPTool<T = unknown>(
60
+ methodName: string,
61
+ args: unknown,
62
+ init?: CustomInit,
63
+ ): Promise<T> {
64
+ const mergedInit: CustomInit = {
65
+ ...init,
66
+ headers: {
67
+ ...DEFAULT_INIT.headers,
68
+ ...init?.headers,
69
+ },
70
+ };
71
+
72
+ const response = await fetch(`/mcp/call-tool/${methodName}`, {
73
+ method: "POST",
74
+ body: JSON.stringify(args),
75
+ credentials: "include",
76
+ ...mergedInit,
77
+ });
78
+
79
+ if (!response.ok) {
80
+ throw new Error(`Failed to call ${methodName}: ${response.statusText}`);
81
+ }
82
+
83
+ return response.json() as Promise<T>;
84
+ }
85
+
86
+ /**
87
+ * Creates a subscribe method for a resource that returns an async iterator
88
+ * yielding {uri, data} objects as resources are updated.
89
+ */
90
+ function createSubscribeMethod(
91
+ resourceName: string,
92
+ init?: CustomInit,
93
+ ): (args: { id: string }) => AsyncIterableIterator<{
94
+ uri: string;
95
+ data: unknown;
96
+ }> {
97
+ return async function* (args: { id: string } | { uri: string }) {
98
+ // Step 1: Call DESCRIBE to get watch endpoint configuration and URI template
99
+ const describeMethodName = `DECO_RESOURCE_${resourceName}_DESCRIBE`;
100
+ const readMethodName = `DECO_RESOURCE_${resourceName}_READ`;
101
+
102
+ // Get describe information
103
+ const describeData = await callMCPTool<{
104
+ uriTemplate?: string;
105
+ features?: {
106
+ watch?: {
107
+ pathname?: string;
108
+ };
109
+ };
110
+ }>(describeMethodName, {}, init);
111
+
112
+ const watchPathname = describeData?.features?.watch?.pathname;
113
+ const uriTemplate = describeData?.uriTemplate;
114
+
115
+ if (!watchPathname) {
116
+ throw new Error(
117
+ `Resource ${resourceName} does not support watch functionality`,
118
+ );
119
+ }
120
+
121
+ if (!uriTemplate) {
122
+ throw new Error(`Resource ${resourceName} does not provide uriTemplate`);
123
+ }
124
+
125
+ // Step 2: Construct URI from template by replacing * with id
126
+ const resourceUri =
127
+ "uri" in args ? args.uri : uriTemplate.replace("*", args.id);
128
+
129
+ // Step 3: Construct watch URL and create EventSource
130
+ const watchUrl = new URL(watchPathname, globalThis.location.origin);
131
+ watchUrl.searchParams.set("uri", resourceUri);
132
+
133
+ const eventSource = new EventSource(watchUrl.href);
134
+
135
+ // Step 4: Use toAsyncIterator to consume SSE events and enrich with READ data
136
+ const eventStream = toAsyncIterator<{ uri: string }>(
137
+ eventSource,
138
+ "message",
139
+ );
140
+
141
+ // Iterate over SSE events and enrich with full data
142
+ for await (const event of eventStream) {
143
+ const uri = event.uri;
144
+
145
+ if (uri) {
146
+ // Call READ to get full resource data
147
+ const readData = await callMCPTool<{ data: unknown }>(
148
+ readMethodName,
149
+ { uri },
150
+ init,
151
+ );
152
+
153
+ yield { uri, data: readData.data };
154
+ }
155
+ }
156
+ };
157
+ }
158
+
159
+ export const createClient = <T>(init?: CustomInit): MCPClient<T> => {
160
+ return new Proxy(
161
+ {},
162
+ {
163
+ get: (_, prop) => {
164
+ const propStr = String(prop);
165
+
166
+ // Check if this is a SUBSCRIBE method call
167
+ const subscribeMatch = propStr.match(/^DECO_RESOURCE_(.+)_SUBSCRIBE$/);
168
+ if (subscribeMatch) {
169
+ const resourceName = subscribeMatch[1];
170
+ return createSubscribeMethod(resourceName, init);
171
+ }
172
+
173
+ // Regular method call
174
+ return async (args: unknown, innerInit?: CustomInit) => {
175
+ const mergedInit: CustomInit = {
176
+ ...init,
177
+ ...innerInit,
178
+ headers: {
179
+ ...DEFAULT_INIT.headers,
180
+ ...init?.headers,
181
+ ...innerInit?.headers,
182
+ },
183
+ };
184
+
185
+ const response = await fetch(`/mcp/call-tool/${String(prop)}`, {
186
+ method: "POST",
187
+ body: JSON.stringify(args),
188
+ credentials: "include",
189
+ ...mergedInit,
190
+ });
191
+
192
+ if (typeof mergedInit.handleResponse === "function") {
193
+ return mergedInit.handleResponse(response);
194
+ }
195
+
196
+ return response.json();
197
+ };
198
+ },
199
+ },
200
+ ) as MCPClient<T>;
201
+ };
@@ -0,0 +1,53 @@
1
+ export type SSEConnection = {
2
+ type: "SSE";
3
+ url: string;
4
+ token?: string;
5
+ headers?: Record<string, string>;
6
+ };
7
+
8
+ export type WebsocketConnection = {
9
+ type: "Websocket";
10
+ url: string;
11
+ token?: string;
12
+ };
13
+
14
+ export type DecoConnection = {
15
+ type: "Deco";
16
+ tenant: string;
17
+ token?: string;
18
+ };
19
+
20
+ export type InnateConnection = {
21
+ type: "INNATE";
22
+ name: string;
23
+ workspace?: string;
24
+ };
25
+
26
+ export type HTTPConnection = {
27
+ type: "HTTP";
28
+ url: string;
29
+ headers?: Record<string, string>;
30
+ token?: string;
31
+ };
32
+
33
+ export type MCPConnection =
34
+ | SSEConnection
35
+ | WebsocketConnection
36
+ | InnateConnection
37
+ | DecoConnection
38
+ | HTTPConnection;
39
+
40
+ export type Integration = {
41
+ /** Unique identifier for the MCP */
42
+ id: string;
43
+ /** Human-readable name of the integration */
44
+ name: string;
45
+ /** Brief description of the integration's functionality */
46
+ description?: string;
47
+ /** URL to the integration's icon */
48
+ icon?: string;
49
+ /** Access level of the integration */
50
+ access?: string | null;
51
+ /** Connection configuration */
52
+ connection: MCPConnection;
53
+ };
package/src/drizzle.ts ADDED
@@ -0,0 +1,201 @@
1
+ import type { DrizzleConfig } from "drizzle-orm";
2
+ import {
3
+ drizzle as drizzleProxy,
4
+ type SqliteRemoteDatabase,
5
+ } from "drizzle-orm/sqlite-proxy";
6
+ import { QueryResult } from "./mcp.ts";
7
+ export * from "drizzle-orm/sqlite-core";
8
+ export * as orm from "drizzle-orm";
9
+ import { sql } from "drizzle-orm";
10
+ import { DefaultEnv } from "./index.ts";
11
+
12
+ const mapGetResult = ({ result: [page] }: { result: QueryResult[] }) => {
13
+ return page.results ?? [];
14
+ };
15
+
16
+ const mapPostResult = ({ result }: { result: QueryResult[] }) => {
17
+ return (
18
+ result
19
+ .map((page) => page.results ?? [])
20
+ .flat()
21
+ // @ts-expect-error - this is ok, result comes as unknown
22
+ .map(Object.values)
23
+ );
24
+ };
25
+
26
+ export function drizzle<
27
+ TSchema extends Record<string, unknown> = Record<string, never>,
28
+ >(
29
+ { DECO_WORKSPACE_DB }: Pick<DefaultEnv, "DECO_WORKSPACE_DB">,
30
+ config?: DrizzleConfig<TSchema>,
31
+ ) {
32
+ return drizzleProxy((sql, params, method) => {
33
+ // https://orm.drizzle.team/docs/connect-drizzle-proxy says
34
+ // Drizzle always waits for {rows: string[][]} or {rows: string[]} for the return value.
35
+ // When the method is get, you should return a value as {rows: string[]}.
36
+ // Otherwise, you should return {rows: string[][]}.
37
+ const asRows = method === "get" ? mapGetResult : mapPostResult;
38
+ return DECO_WORKSPACE_DB.query({
39
+ sql,
40
+ params,
41
+ }).then((result) => ({ rows: asRows(result) }));
42
+ }, config);
43
+ }
44
+
45
+ /**
46
+ * The following code is a custom migration system tweaked
47
+ * from the durable-sqlite original migrator.
48
+ *
49
+ * @see https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/durable-sqlite/migrator.ts
50
+ *
51
+ * It applies the migrations without transactions, as a workaround
52
+ * while we don't have remote transactions support on the
53
+ * workspace database durable object. Not ideal and we should
54
+ * look into implementing some way of doing transactions soon.
55
+ */
56
+
57
+ export interface MigrationMeta {
58
+ sql: string[];
59
+ folderMillis: number;
60
+ hash: string;
61
+ bps: boolean;
62
+ }
63
+
64
+ export interface MigrationConfig {
65
+ journal: {
66
+ entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
67
+ };
68
+ migrations: Record<string, string>;
69
+ debug?: boolean;
70
+ }
71
+
72
+ function readMigrationFiles({
73
+ journal,
74
+ migrations,
75
+ }: MigrationConfig): MigrationMeta[] {
76
+ const migrationQueries: MigrationMeta[] = [];
77
+
78
+ for (const journalEntry of journal.entries) {
79
+ const query =
80
+ migrations[`m${journalEntry.idx.toString().padStart(4, "0")}`];
81
+
82
+ if (!query) {
83
+ throw new Error(`Missing migration: ${journalEntry.tag}`);
84
+ }
85
+
86
+ try {
87
+ const result = query.split("--> statement-breakpoint").map((it) => {
88
+ return it;
89
+ });
90
+
91
+ migrationQueries.push({
92
+ sql: result,
93
+ bps: journalEntry.breakpoints,
94
+ folderMillis: journalEntry.when,
95
+ hash: "",
96
+ });
97
+ } catch {
98
+ throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
99
+ }
100
+ }
101
+
102
+ return migrationQueries;
103
+ }
104
+
105
+ export async function migrateWithoutTransaction(
106
+ db: SqliteRemoteDatabase,
107
+ config: MigrationConfig,
108
+ ): Promise<void> {
109
+ const debug = config.debug ?? false;
110
+
111
+ if (debug) console.log("Migrating database");
112
+ const migrations = readMigrationFiles(config);
113
+ if (debug) console.log("Migrations", migrations);
114
+
115
+ try {
116
+ if (debug) console.log("Setting up migrations table");
117
+ const migrationsTable = "__drizzle_migrations";
118
+
119
+ // Create migrations table if it doesn't exist
120
+ // Note: Changed from SERIAL to INTEGER PRIMARY KEY AUTOINCREMENT for SQLite compatibility
121
+ const migrationTableCreate = sql`
122
+ CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ hash text NOT NULL,
125
+ created_at numeric
126
+ )
127
+ `;
128
+ await db.run(migrationTableCreate);
129
+
130
+ // Get the last applied migration
131
+ const dbMigrations = await db.values<[number, string, string]>(
132
+ sql`SELECT id, hash, created_at FROM ${sql.identifier(
133
+ migrationsTable,
134
+ )} ORDER BY created_at DESC LIMIT 1`,
135
+ );
136
+
137
+ const lastDbMigration = dbMigrations[0] ?? undefined;
138
+ if (debug) console.log("Last applied migration:", lastDbMigration);
139
+
140
+ // Apply pending migrations sequentially (without transaction wrapper)
141
+ for (const migration of migrations) {
142
+ const hasNoMigrations =
143
+ lastDbMigration === undefined || !lastDbMigration.length;
144
+ if (
145
+ hasNoMigrations ||
146
+ Number(lastDbMigration[2])! < migration.folderMillis
147
+ ) {
148
+ if (debug) console.log(`Applying migration: ${migration.folderMillis}`);
149
+
150
+ try {
151
+ // Execute all statements in the migration
152
+ for (const stmt of migration.sql) {
153
+ if (stmt.trim()) {
154
+ // Skip empty statements
155
+ if (debug) {
156
+ console.log("Executing:", stmt.substring(0, 100) + "...");
157
+ }
158
+ await db.run(sql.raw(stmt));
159
+ }
160
+ }
161
+
162
+ // Record successful migration
163
+ await db.run(
164
+ sql`INSERT INTO ${sql.identifier(
165
+ migrationsTable,
166
+ )} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
167
+ );
168
+
169
+ if (debug) {
170
+ console.log(
171
+ `✅ Migration ${migration.folderMillis} applied successfully`,
172
+ );
173
+ }
174
+ } catch (migrationError: unknown) {
175
+ console.error(
176
+ `❌ Migration ${migration.folderMillis} failed:`,
177
+ migrationError,
178
+ );
179
+ throw new Error(
180
+ `Migration failed at ${migration.folderMillis}: ${
181
+ migrationError instanceof Error
182
+ ? migrationError.message
183
+ : String(migrationError)
184
+ }`,
185
+ );
186
+ }
187
+ } else {
188
+ if (debug) {
189
+ console.log(
190
+ `⏭️ Skipping already applied migration: ${migration.folderMillis}`,
191
+ );
192
+ }
193
+ }
194
+ }
195
+
196
+ if (debug) console.log("✅ All migrations completed successfully");
197
+ } catch (error: unknown) {
198
+ console.error("❌ Migration process failed:", error);
199
+ throw error;
200
+ }
201
+ }
@@ -0,0 +1,66 @@
1
+ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ StreamableHTTPClientTransport,
4
+ type StreamableHTTPClientTransportOptions,
5
+ } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+
7
+ export class HTTPClientTransport extends StreamableHTTPClientTransport {
8
+ constructor(url: URL, opts?: StreamableHTTPClientTransportOptions) {
9
+ super(url, opts);
10
+ }
11
+
12
+ override send(
13
+ message: JSONRPCMessage,
14
+ options?: {
15
+ resumptionToken?: string;
16
+ onresumptiontoken?: (token: string) => void;
17
+ },
18
+ ): Promise<void> {
19
+ const mockAction = getMockActionFor(message);
20
+ if (mockAction?.type === "emit") {
21
+ this.onmessage?.(mockAction.message);
22
+ return Promise.resolve();
23
+ }
24
+ if (mockAction?.type === "suppress") {
25
+ return Promise.resolve();
26
+ }
27
+ return super.send(message, options);
28
+ }
29
+ }
30
+
31
+ type MockAction =
32
+ | { type: "emit"; message: JSONRPCMessage }
33
+ | { type: "suppress" };
34
+
35
+ function getMockActionFor(message: JSONRPCMessage): MockAction | null {
36
+ const m = message;
37
+ if (!m || typeof m !== "object" || !("method" in m)) return null;
38
+
39
+ switch (m.method) {
40
+ case "initialize": {
41
+ const protocolVersion = m?.params?.protocolVersion;
42
+ if (!protocolVersion) return null;
43
+ return {
44
+ type: "emit",
45
+ message: {
46
+ result: {
47
+ protocolVersion,
48
+ capabilities: { tools: {} },
49
+ serverInfo: { name: "deco-chat-server", version: "1.0.0" },
50
+ },
51
+ jsonrpc: m.jsonrpc ?? "2.0",
52
+ // @ts-expect-error - id is not typed
53
+ id: m.id,
54
+ } as JSONRPCMessage,
55
+ };
56
+ }
57
+ case "notifications/roots/list_changed":
58
+ case "notifications/initialized":
59
+ case "notifications/cancelled":
60
+ case "notifications/progress": {
61
+ return { type: "suppress" };
62
+ }
63
+ default:
64
+ return null;
65
+ }
66
+ }