@cerefox/memory 0.7.2 → 0.8.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/bin/cerefox.js +1096 -344
- package/dist/frontend/assets/{index-BzAPcCXA.js → index-CAp2_lFX.js} +2 -2
- package/dist/frontend/assets/index-CAp2_lFX.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +97 -0
- package/dist/server-assets/_shared/embeddings/index.ts +175 -0
- package/dist/server-assets/_shared/mcp-tools/_chunker.ts +187 -0
- package/dist/server-assets/_shared/mcp-tools/_projects.ts +121 -0
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +73 -0
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +95 -0
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +73 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +26 -0
- package/dist/server-assets/_shared/mcp-tools/get-help.ts +90 -0
- package/dist/server-assets/_shared/mcp-tools/index.ts +67 -0
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +315 -0
- package/dist/server-assets/_shared/mcp-tools/list-metadata-keys.ts +55 -0
- package/dist/server-assets/_shared/mcp-tools/list-projects.ts +59 -0
- package/dist/server-assets/_shared/mcp-tools/list-versions.ts +72 -0
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +154 -0
- package/dist/server-assets/_shared/mcp-tools/search.ts +193 -0
- package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +163 -0
- package/dist/server-assets/_shared/mcp-tools/types.ts +92 -0
- package/dist/server-assets/db/migrations/0003_add_document_versions.sql +91 -0
- package/dist/server-assets/db/migrations/0004_add_audit_log_review_status_archived.sql +71 -0
- package/dist/server-assets/db/migrations/0005_metadata_search.sql +628 -0
- package/dist/server-assets/db/migrations/0006_usage_log.sql +255 -0
- package/dist/server-assets/db/migrations/0007_usage_log_requestor.sql +178 -0
- package/dist/server-assets/db/migrations/0008_soft_delete.sql +130 -0
- package/dist/server-assets/db/migrations/0009_audit_log_restore_operation.sql +20 -0
- package/dist/server-assets/db/migrations/0010_requestor_enforcement_config.sql +12 -0
- package/dist/server-assets/db/migrations/0011_title_boosting.sql +48 -0
- package/dist/server-assets/db/rpcs.sql +1723 -0
- package/dist/server-assets/db/schema.sql +380 -0
- package/dist/server-assets/supabase/functions/cerefox-get-audit-log/index.ts +117 -0
- package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +138 -0
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +819 -0
- package/dist/server-assets/supabase/functions/cerefox-list-projects/index.ts +96 -0
- package/dist/server-assets/supabase/functions/cerefox-list-versions/index.ts +113 -0
- package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +294 -0
- package/dist/server-assets/supabase/functions/cerefox-mcp/shared.ts +42 -0
- package/dist/server-assets/supabase/functions/cerefox-metadata/index.ts +99 -0
- package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +146 -0
- package/dist/server-assets/supabase/functions/cerefox-search/index.ts +382 -0
- package/docs/guides/connect-agents.md +58 -3
- package/docs/guides/migration-v0.5.md +50 -0
- package/package.json +3 -2
- package/dist/frontend/assets/index-BzAPcCXA.js.map +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* cerefox-list-projects -- Supabase Edge Function
|
|
7
|
+
*
|
|
8
|
+
* Lists all projects with names, IDs, and descriptions.
|
|
9
|
+
* Calls the cerefox_list_projects() RPC via the service-role key.
|
|
10
|
+
*
|
|
11
|
+
* Called by:
|
|
12
|
+
* - GPT Custom Actions (direct HTTP POST via OpenAPI schema)
|
|
13
|
+
* - Any authenticated HTTP client
|
|
14
|
+
*
|
|
15
|
+
* Note: cerefox-mcp calls the RPC directly (not this Edge Function).
|
|
16
|
+
*
|
|
17
|
+
* Request body (JSON): {} or { requestor?: string }
|
|
18
|
+
* Response (200): Array of { id, name, description }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const CORS_HEADERS = {
|
|
22
|
+
"Access-Control-Allow-Origin": "*",
|
|
23
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
24
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
Deno.serve(async (req: Request): Promise<Response> => {
|
|
28
|
+
if (req.method === "OPTIONS") {
|
|
29
|
+
return new Response(null, { status: 200, headers: CORS_HEADERS });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isVersionRequest(req)) {
|
|
33
|
+
return versionResponse("cerefox-list-projects", { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method !== "POST") {
|
|
37
|
+
return new Response("Method Not Allowed", { status: 405, headers: CORS_HEADERS });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const body = await req.json().catch(() => ({}));
|
|
42
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
43
|
+
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
44
|
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
45
|
+
|
|
46
|
+
// Configurable requestor enforcement
|
|
47
|
+
const identityField = "requestor";
|
|
48
|
+
const identityValue = body[identityField];
|
|
49
|
+
const { data: reqConfig } = await supabase.rpc("cerefox_get_config", { p_key: "require_requestor_identity" });
|
|
50
|
+
if (reqConfig === "true") {
|
|
51
|
+
if (!identityValue || (typeof identityValue === "string" && identityValue.trim() === "")) {
|
|
52
|
+
return new Response(
|
|
53
|
+
JSON.stringify({ error: `Missing required parameter "${identityField}". Server requires caller identity.` }),
|
|
54
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const { data: fmtConfig } = await supabase.rpc("cerefox_get_config", { p_key: "requestor_identity_format" });
|
|
58
|
+
if (fmtConfig && typeof fmtConfig === "string" && fmtConfig.trim() !== "") {
|
|
59
|
+
if (!new RegExp(fmtConfig).test(identityValue)) {
|
|
60
|
+
return new Response(
|
|
61
|
+
JSON.stringify({ error: `Invalid "${identityField}" format. Does not match pattern: ${fmtConfig}` }),
|
|
62
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { data, error } = await supabase.rpc("cerefox_list_projects");
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
72
|
+
status: 500,
|
|
73
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fire-and-forget usage logging
|
|
78
|
+
Promise.resolve(supabase.rpc("cerefox_log_usage", {
|
|
79
|
+
p_operation: "list_projects",
|
|
80
|
+
p_access_path: "edge-function",
|
|
81
|
+
p_requestor: body.requestor ?? null,
|
|
82
|
+
p_result_count: (data ?? []).length,
|
|
83
|
+
})).catch(() => {});
|
|
84
|
+
|
|
85
|
+
return new Response(JSON.stringify(data ?? []), {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
92
|
+
status: 500,
|
|
93
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* cerefox-list-versions — Supabase Edge Function
|
|
7
|
+
*
|
|
8
|
+
* Lists all archived versions of a document, newest first.
|
|
9
|
+
* Calls the cerefox_list_document_versions RPC via the service-role key.
|
|
10
|
+
*
|
|
11
|
+
* Called by:
|
|
12
|
+
* - cerefox-mcp Edge Function (MCP tools/call for cerefox_list_versions)
|
|
13
|
+
* - GPT Custom Actions (direct HTTP POST via OpenAPI schema)
|
|
14
|
+
* - Any authenticated HTTP client
|
|
15
|
+
*
|
|
16
|
+
* Request body (JSON):
|
|
17
|
+
* { document_id: string }
|
|
18
|
+
*
|
|
19
|
+
* Response (200):
|
|
20
|
+
* Array of { version_id, version_number, source, chunk_count, total_chars, created_at }
|
|
21
|
+
* Empty array [] when no versions exist.
|
|
22
|
+
* Response (400):
|
|
23
|
+
* { error: "document_id is required" }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const CORS_HEADERS = {
|
|
27
|
+
"Access-Control-Allow-Origin": "*",
|
|
28
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
29
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
Deno.serve(async (req: Request): Promise<Response> => {
|
|
33
|
+
if (req.method === "OPTIONS") {
|
|
34
|
+
return new Response(null, { status: 200, headers: CORS_HEADERS });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isVersionRequest(req)) {
|
|
38
|
+
return versionResponse("cerefox-list-versions", { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (req.method !== "POST") {
|
|
42
|
+
return new Response("Method Not Allowed", { status: 405, headers: CORS_HEADERS });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const body = await req.json();
|
|
47
|
+
const document_id = body.document_id as string | undefined;
|
|
48
|
+
|
|
49
|
+
if (!document_id) {
|
|
50
|
+
return new Response(JSON.stringify({ error: "document_id is required" }), {
|
|
51
|
+
status: 400,
|
|
52
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
57
|
+
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
58
|
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
59
|
+
|
|
60
|
+
// Configurable requestor enforcement
|
|
61
|
+
const identityField = "requestor";
|
|
62
|
+
const identityValue = body[identityField];
|
|
63
|
+
const { data: reqConfig } = await supabase.rpc("cerefox_get_config", { p_key: "require_requestor_identity" });
|
|
64
|
+
if (reqConfig === "true") {
|
|
65
|
+
if (!identityValue || (typeof identityValue === "string" && identityValue.trim() === "")) {
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({ error: `Missing required parameter "${identityField}". Server requires caller identity.` }),
|
|
68
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const { data: fmtConfig } = await supabase.rpc("cerefox_get_config", { p_key: "requestor_identity_format" });
|
|
72
|
+
if (fmtConfig && typeof fmtConfig === "string" && fmtConfig.trim() !== "") {
|
|
73
|
+
if (!new RegExp(fmtConfig).test(identityValue)) {
|
|
74
|
+
return new Response(
|
|
75
|
+
JSON.stringify({ error: `Invalid "${identityField}" format. Does not match pattern: ${fmtConfig}` }),
|
|
76
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { data, error } = await supabase.rpc("cerefox_list_document_versions", {
|
|
83
|
+
p_document_id: document_id,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (error) {
|
|
87
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
88
|
+
status: 500,
|
|
89
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fire-and-forget usage logging
|
|
94
|
+
Promise.resolve(supabase.rpc("cerefox_log_usage", {
|
|
95
|
+
p_operation: "list_versions",
|
|
96
|
+
p_access_path: "edge-function",
|
|
97
|
+
p_requestor: body.requestor ?? null,
|
|
98
|
+
p_document_id: document_id,
|
|
99
|
+
p_result_count: (data ?? []).length,
|
|
100
|
+
})).catch(() => {});
|
|
101
|
+
|
|
102
|
+
return new Response(JSON.stringify(data ?? []), {
|
|
103
|
+
status: 200,
|
|
104
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
109
|
+
status: 500,
|
|
110
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cerefox-mcp — Supabase Edge Function
|
|
5
|
+
*
|
|
6
|
+
* MCP Streamable HTTP server (spec 2025-03-26). Exposes all Cerefox tools
|
|
7
|
+
* over HTTPS — no Python install, no local process, works from any
|
|
8
|
+
* remote-capable MCP client.
|
|
9
|
+
*
|
|
10
|
+
* As of v0.4.0 (iter-22): the per-tool handlers live in `_shared/mcp-tools/`
|
|
11
|
+
* (relative to the repo root) and are imported here verbatim. The new local
|
|
12
|
+
* TS MCP server (`@cerefox/memory`, `packages/memory/`) uses the same
|
|
13
|
+
* modules — single source of truth for tool behaviour across the two
|
|
14
|
+
* transports. This file's only responsibility is the MCP protocol surface
|
|
15
|
+
* (JSON-RPC over HTTP) + Cerefox's identity-enforcement wrapper.
|
|
16
|
+
*
|
|
17
|
+
* Supported clients:
|
|
18
|
+
* Claude Code -- claude mcp add --transport http cerefox <url> --header "Authorization: Bearer <anon-key>"
|
|
19
|
+
* Cursor -- url + headers.Authorization in mcp.json
|
|
20
|
+
* Claude Desktop -- npx supergateway --streamableHttp <url> --header "Authorization: Bearer <anon-key>"
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
CORS_HEADERS,
|
|
25
|
+
errorResponse,
|
|
26
|
+
jsonResponse,
|
|
27
|
+
makeSupabaseClient,
|
|
28
|
+
notificationResponse,
|
|
29
|
+
} from "./shared.ts";
|
|
30
|
+
import {
|
|
31
|
+
ALL_TOOLS,
|
|
32
|
+
McpInvalidParams,
|
|
33
|
+
TOOLS_BY_NAME,
|
|
34
|
+
type ToolContext,
|
|
35
|
+
} from "../../../_shared/mcp-tools/index.ts";
|
|
36
|
+
import {
|
|
37
|
+
type AggregatedVersions,
|
|
38
|
+
EF_VERSION,
|
|
39
|
+
isVersionRequest,
|
|
40
|
+
PEER_EF_NAMES,
|
|
41
|
+
peerVersionUrl,
|
|
42
|
+
versionResponse,
|
|
43
|
+
wantsPeers,
|
|
44
|
+
} from "../../../_shared/ef-meta/index.ts";
|
|
45
|
+
|
|
46
|
+
const MCP_VERSION = "2025-03-26";
|
|
47
|
+
const SERVER_NAME = "cerefox";
|
|
48
|
+
const SERVER_VERSION = "0.4.0";
|
|
49
|
+
|
|
50
|
+
// ── Tool list (derived from _shared/mcp-tools/) ─────────────────────────────
|
|
51
|
+
|
|
52
|
+
const TOOLS = ALL_TOOLS.map((t) => ({
|
|
53
|
+
name: t.name,
|
|
54
|
+
description: t.description,
|
|
55
|
+
inputSchema: t.inputSchema,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// ── Method handlers ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function handleInitialize(id: unknown): Response {
|
|
61
|
+
return jsonResponse({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
id,
|
|
64
|
+
result: {
|
|
65
|
+
protocolVersion: MCP_VERSION,
|
|
66
|
+
capabilities: { tools: {} },
|
|
67
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleToolsList(id: unknown): Response {
|
|
73
|
+
return jsonResponse({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleToolsCall(
|
|
77
|
+
id: unknown,
|
|
78
|
+
params: { name?: string; arguments?: Record<string, unknown> } | undefined,
|
|
79
|
+
): Promise<Response> {
|
|
80
|
+
const toolName = params?.name;
|
|
81
|
+
const args = params?.arguments ?? {};
|
|
82
|
+
|
|
83
|
+
if (!toolName) return errorResponse(id, -32602, "Invalid params: missing tool name");
|
|
84
|
+
|
|
85
|
+
const tool = TOOLS_BY_NAME[toolName];
|
|
86
|
+
if (!tool) return errorResponse(id, -32602, `Unknown tool: ${toolName}`);
|
|
87
|
+
|
|
88
|
+
// Configurable caller identity enforcement.
|
|
89
|
+
// When require_requestor_identity is "true" in cerefox_config, all tool calls
|
|
90
|
+
// must include a requestor (reads) or author (writes) parameter.
|
|
91
|
+
// When requestor_identity_format is set, the value must match the regex.
|
|
92
|
+
const identityParam = toolName === "cerefox_ingest" || toolName === "cerefox_set_document_projects"
|
|
93
|
+
? "author"
|
|
94
|
+
: "requestor";
|
|
95
|
+
const identityValue = args[identityParam] as string | undefined;
|
|
96
|
+
|
|
97
|
+
// deno-lint-ignore no-explicit-any
|
|
98
|
+
const supabase: any = makeSupabaseClient();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { data: requireConfig } = await supabase.rpc("cerefox_get_config", {
|
|
102
|
+
p_key: "require_requestor_identity",
|
|
103
|
+
});
|
|
104
|
+
const requireIdentity = requireConfig === "true";
|
|
105
|
+
|
|
106
|
+
if (requireIdentity) {
|
|
107
|
+
if (!identityValue || identityValue.trim() === "") {
|
|
108
|
+
return errorResponse(
|
|
109
|
+
id,
|
|
110
|
+
-32602,
|
|
111
|
+
`Missing required parameter "${identityParam}". Server requires caller identity. ` +
|
|
112
|
+
`Pass "${identityParam}" with your agent name (e.g., "Claude Code", "archiver").`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const { data: formatConfig } = await supabase.rpc("cerefox_get_config", {
|
|
116
|
+
p_key: "requestor_identity_format",
|
|
117
|
+
});
|
|
118
|
+
if (formatConfig && typeof formatConfig === "string" && formatConfig.trim() !== "") {
|
|
119
|
+
const formatRegex = new RegExp(formatConfig);
|
|
120
|
+
if (!formatRegex.test(identityValue)) {
|
|
121
|
+
return errorResponse(
|
|
122
|
+
id,
|
|
123
|
+
-32602,
|
|
124
|
+
`Invalid "${identityParam}" format. Value "${identityValue}" does not match ` +
|
|
125
|
+
`required pattern: ${formatConfig}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Config check failed -- don't block the tool call
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const openaiKey = Deno.env.get("OPENAI_API_KEY");
|
|
135
|
+
const needsOpenAI = toolName === "cerefox_search" || toolName === "cerefox_ingest";
|
|
136
|
+
if (needsOpenAI && !openaiKey) {
|
|
137
|
+
return errorResponse(id, -32603, "OPENAI_API_KEY secret not set on this project");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ctx: ToolContext = {
|
|
141
|
+
accessPath: "remote-mcp",
|
|
142
|
+
openaiApiKey: openaiKey,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const text = await tool.handler(supabase, args, ctx);
|
|
147
|
+
return jsonResponse({
|
|
148
|
+
jsonrpc: "2.0",
|
|
149
|
+
id,
|
|
150
|
+
result: { content: [{ type: "text", text }] },
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
const code = err instanceof McpInvalidParams ? -32602 : -32603;
|
|
155
|
+
return errorResponse(id, code, message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Version surface (iter-26 Part 26B) ───────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
const JSON_HEADERS = { ...CORS_HEADERS, "Content-Type": "application/json" };
|
|
162
|
+
|
|
163
|
+
/** Overall budget for the peer-version aggregator. */
|
|
164
|
+
const AGGREGATOR_BUDGET_MS = 5_000;
|
|
165
|
+
/** Per-peer request timeout (so one slow peer can't eat the whole budget). */
|
|
166
|
+
const PEER_TIMEOUT_MS = 2_000;
|
|
167
|
+
|
|
168
|
+
async function handleVersion(req: Request): Promise<Response> {
|
|
169
|
+
// Single-EF version unless ?peers=true requests the aggregator.
|
|
170
|
+
if (!wantsPeers(req)) {
|
|
171
|
+
return versionResponse("cerefox-mcp", JSON_HEADERS);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result: AggregatedVersions = {
|
|
175
|
+
name: "cerefox-mcp",
|
|
176
|
+
version: EF_VERSION,
|
|
177
|
+
schema: null,
|
|
178
|
+
efs: [],
|
|
179
|
+
errors: [],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Deployed Postgres schema version (best-effort; null on failure).
|
|
183
|
+
try {
|
|
184
|
+
// deno-lint-ignore no-explicit-any
|
|
185
|
+
const supabase: any = makeSupabaseClient();
|
|
186
|
+
const { data } = await supabase.rpc("cerefox_schema_version");
|
|
187
|
+
if (typeof data === "string") result.schema = data;
|
|
188
|
+
} catch {
|
|
189
|
+
// leave schema null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Probe peers sequentially within an overall budget. Forward the caller's
|
|
193
|
+
// auth so the gateway lets each peer request through.
|
|
194
|
+
const authHeader = req.headers.get("Authorization") ?? "";
|
|
195
|
+
const apikeyHeader = req.headers.get("apikey") ?? "";
|
|
196
|
+
const deadline = Date.now() + AGGREGATOR_BUDGET_MS;
|
|
197
|
+
|
|
198
|
+
for (const peer of PEER_EF_NAMES) {
|
|
199
|
+
const remaining = deadline - Date.now();
|
|
200
|
+
if (remaining <= 0) {
|
|
201
|
+
result.errors.push({ name: peer, error: "skipped (5s budget exhausted)" });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const ctrl = new AbortController();
|
|
206
|
+
const timer = setTimeout(() => ctrl.abort(), Math.min(remaining, PEER_TIMEOUT_MS));
|
|
207
|
+
let resp: Response;
|
|
208
|
+
try {
|
|
209
|
+
resp = await fetch(peerVersionUrl(req.url, peer), {
|
|
210
|
+
method: "GET",
|
|
211
|
+
headers: { Authorization: authHeader, apikey: apikeyHeader },
|
|
212
|
+
signal: ctrl.signal,
|
|
213
|
+
});
|
|
214
|
+
} finally {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
}
|
|
217
|
+
if (!resp.ok) {
|
|
218
|
+
result.errors.push({ name: peer, error: `HTTP ${resp.status}` });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const payload = (await resp.json()) as { version?: string };
|
|
222
|
+
result.efs.push({ name: peer, version: payload.version ?? "unknown" });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
result.errors.push({
|
|
225
|
+
name: peer,
|
|
226
|
+
error: err instanceof Error ? err.message : String(err),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new Response(JSON.stringify(result), { status: 200, headers: JSON_HEADERS });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Main handler ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
Deno.serve(async (req: Request): Promise<Response> => {
|
|
237
|
+
if (req.method === "OPTIONS") {
|
|
238
|
+
return new Response(null, { status: 200, headers: CORS_HEADERS });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// GET — the only supported GET is the /version surface (iter-26). Per MCP
|
|
242
|
+
// spec (2025-03-26) this server otherwise returns 405 on GET to signal it
|
|
243
|
+
// does not support SSE notifications (prevents MCP clients from holding a
|
|
244
|
+
// persistent ~1/sec polling connection).
|
|
245
|
+
if (req.method === "GET") {
|
|
246
|
+
if (isVersionRequest(req)) {
|
|
247
|
+
return await handleVersion(req);
|
|
248
|
+
}
|
|
249
|
+
return new Response("Method Not Allowed", { status: 405, headers: CORS_HEADERS });
|
|
250
|
+
}
|
|
251
|
+
if (req.method !== "POST") {
|
|
252
|
+
return new Response("Method Not Allowed", { status: 405, headers: CORS_HEADERS });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let body: unknown;
|
|
256
|
+
try {
|
|
257
|
+
body = await req.json();
|
|
258
|
+
} catch {
|
|
259
|
+
return errorResponse(null, -32700, "Parse error: invalid JSON");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { jsonrpc, id, method, params } = body as {
|
|
263
|
+
jsonrpc?: string;
|
|
264
|
+
id?: unknown;
|
|
265
|
+
method?: string;
|
|
266
|
+
params?: unknown;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (jsonrpc !== "2.0") {
|
|
270
|
+
return errorResponse(id ?? null, -32600, "Invalid Request: jsonrpc must be '2.0'");
|
|
271
|
+
}
|
|
272
|
+
if (!method) {
|
|
273
|
+
return errorResponse(id ?? null, -32600, "Invalid Request: missing method");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
switch (method) {
|
|
277
|
+
case "initialize":
|
|
278
|
+
return handleInitialize(id);
|
|
279
|
+
case "initialized":
|
|
280
|
+
case "notifications/initialized":
|
|
281
|
+
return notificationResponse();
|
|
282
|
+
case "ping":
|
|
283
|
+
return jsonResponse({ jsonrpc: "2.0", id, result: {} });
|
|
284
|
+
case "tools/list":
|
|
285
|
+
return handleToolsList(id);
|
|
286
|
+
case "tools/call":
|
|
287
|
+
return await handleToolsCall(
|
|
288
|
+
id,
|
|
289
|
+
params as { name?: string; arguments?: Record<string, unknown> } | undefined,
|
|
290
|
+
);
|
|
291
|
+
default:
|
|
292
|
+
return errorResponse(id ?? null, -32601, `Method not found: ${method}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
|
|
4
|
+
// ── CORS ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export const CORS_HEADERS = {
|
|
7
|
+
"Access-Control-Allow-Origin": "*",
|
|
8
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
9
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ── Supabase client (service-role key, bypasses RLS) ─────────────────────────
|
|
13
|
+
|
|
14
|
+
export function makeSupabaseClient() {
|
|
15
|
+
const url = Deno.env.get("SUPABASE_URL")!;
|
|
16
|
+
const key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
17
|
+
return createClient(url, key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── JSON-RPC response helpers ─────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export function jsonResponse(body: unknown, status = 200): Response {
|
|
23
|
+
return new Response(JSON.stringify(body), {
|
|
24
|
+
status,
|
|
25
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function errorResponse(id: unknown, code: number, message: string): Response {
|
|
30
|
+
return jsonResponse(
|
|
31
|
+
{
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
id: id ?? null,
|
|
34
|
+
error: { code, message },
|
|
35
|
+
},
|
|
36
|
+
200, // MCP errors are still HTTP 200 per spec; the error is in the payload
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function notificationResponse(): Response {
|
|
41
|
+
return new Response(null, { status: 202, headers: CORS_HEADERS });
|
|
42
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* cerefox-metadata — Supabase Edge Function
|
|
7
|
+
*
|
|
8
|
+
* Returns all metadata keys currently in use across documents in the
|
|
9
|
+
* Cerefox knowledge base. Calls the cerefox_list_metadata_keys() RPC
|
|
10
|
+
* which derives keys from actual doc_metadata JSONB — no registry table.
|
|
11
|
+
*
|
|
12
|
+
* Called by the cerefox-mcp Edge Function (MCP tool), GPT Actions
|
|
13
|
+
* (direct HTTP POST), or any HTTP client.
|
|
14
|
+
*
|
|
15
|
+
* Request body (JSON): {} (no parameters)
|
|
16
|
+
*
|
|
17
|
+
* Response: Array of { key, doc_count, example_values }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CORS_HEADERS = {
|
|
21
|
+
"Access-Control-Allow-Origin": "*",
|
|
22
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
23
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
Deno.serve(async (req: Request): Promise<Response> => {
|
|
27
|
+
// CORS preflight
|
|
28
|
+
if (req.method === "OPTIONS") {
|
|
29
|
+
return new Response(null, { status: 200, headers: CORS_HEADERS });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isVersionRequest(req)) {
|
|
33
|
+
return versionResponse("cerefox-metadata", { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method !== "POST") {
|
|
37
|
+
return new Response("Method Not Allowed", {
|
|
38
|
+
status: 405,
|
|
39
|
+
headers: CORS_HEADERS,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const body = await req.json().catch(() => ({}));
|
|
45
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
46
|
+
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
47
|
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
48
|
+
|
|
49
|
+
// Configurable requestor enforcement
|
|
50
|
+
const identityField = "requestor";
|
|
51
|
+
const identityValue = body[identityField];
|
|
52
|
+
const { data: reqConfig } = await supabase.rpc("cerefox_get_config", { p_key: "require_requestor_identity" });
|
|
53
|
+
if (reqConfig === "true") {
|
|
54
|
+
if (!identityValue || (typeof identityValue === "string" && identityValue.trim() === "")) {
|
|
55
|
+
return new Response(
|
|
56
|
+
JSON.stringify({ error: `Missing required parameter "${identityField}". Server requires caller identity.` }),
|
|
57
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const { data: fmtConfig } = await supabase.rpc("cerefox_get_config", { p_key: "requestor_identity_format" });
|
|
61
|
+
if (fmtConfig && typeof fmtConfig === "string" && fmtConfig.trim() !== "") {
|
|
62
|
+
if (!new RegExp(fmtConfig).test(identityValue)) {
|
|
63
|
+
return new Response(
|
|
64
|
+
JSON.stringify({ error: `Invalid "${identityField}" format. Does not match pattern: ${fmtConfig}` }),
|
|
65
|
+
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { data, error } = await supabase.rpc("cerefox_list_metadata_keys");
|
|
72
|
+
|
|
73
|
+
if (error) {
|
|
74
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
75
|
+
status: 500,
|
|
76
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fire-and-forget usage logging
|
|
81
|
+
Promise.resolve(supabase.rpc("cerefox_log_usage", {
|
|
82
|
+
p_operation: "list_metadata_keys",
|
|
83
|
+
p_access_path: "edge-function",
|
|
84
|
+
p_requestor: body.requestor ?? null,
|
|
85
|
+
p_result_count: (data ?? []).length,
|
|
86
|
+
})).catch(() => {});
|
|
87
|
+
|
|
88
|
+
return new Response(JSON.stringify(data ?? []), {
|
|
89
|
+
status: 200,
|
|
90
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
95
|
+
status: 500,
|
|
96
|
+
headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|