From 5d47fb80197670d4b3dbf18f69d6622921f3637b Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 4 May 2026 12:27:48 -0700 Subject: [PATCH] Normalize mcp plugin secret/connection refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move secret/connection references out of the JSON `mcp_source.config` column into: - Flat columns on mcp_source: auth_kind, auth_header_name, auth_secret_id, auth_secret_prefix, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id (each indexed where it carries a ref). Replaces the McpConnectionAuth json blob. - mcp_source_header / mcp_source_query_param: child tables for the remote source's SecretBackedMap entries (same shape as graphql / openapi child tables). The remaining structural fields (transport, endpoint, command, args, etc.) stay as JSON in `config` because they're plugin-private and vary by transport. mcp_binding.binding stays JSON too — McpToolBinding carries no refs and inputSchema/outputSchema are arbitrary user JSON. Plugin gains usagesForSecret / usagesForConnection. Migration 0009 backfills via json_extract / json_each, then strips the extracted fields with json_remove. Three migration tests cover backfill from a hand-seeded pre-migration DB; two plugin-level tests cover usage fan-out across header-auth and oauth2 sources. --- apps/cloud/drizzle/0010_normalize_mcp.sql | 132 + apps/cloud/drizzle/meta/0010_snapshot.json | 2594 +++++++++++++++++ apps/cloud/drizzle/meta/_journal.json | 7 + apps/cloud/src/services/executor-schema.ts | 43 + apps/local/drizzle/0009_normalize_mcp.sql | 161 + apps/local/drizzle/meta/0009_snapshot.json | 2173 ++++++++++++++ apps/local/drizzle/meta/_journal.json | 7 + apps/local/src/server/executor-schema.ts | 43 + .../src/server/migrate-connections.test.ts | 20 +- apps/local/src/server/migrate-connections.ts | 37 +- .../server/migrate-graphql-bindings.test.ts | 18 + .../src/server/migrate-mcp-bindings.test.ts | 234 ++ .../server/migrate-openapi-bindings.test.ts | 18 + packages/plugins/mcp/src/sdk/binding-store.ts | 488 +++- packages/plugins/mcp/src/sdk/plugin.test.ts | 76 + packages/plugins/mcp/src/sdk/plugin.ts | 67 + 16 files changed, 6101 insertions(+), 17 deletions(-) create mode 100644 apps/cloud/drizzle/0010_normalize_mcp.sql create mode 100644 apps/cloud/drizzle/meta/0010_snapshot.json create mode 100644 apps/local/drizzle/0009_normalize_mcp.sql create mode 100644 apps/local/drizzle/meta/0009_snapshot.json create mode 100644 apps/local/src/server/migrate-mcp-bindings.test.ts diff --git a/apps/cloud/drizzle/0010_normalize_mcp.sql b/apps/cloud/drizzle/0010_normalize_mcp.sql new file mode 100644 index 000000000..6746cf2a6 --- /dev/null +++ b/apps/cloud/drizzle/0010_normalize_mcp.sql @@ -0,0 +1,132 @@ +-- Normalize mcp plugin: lift the McpConnectionAuth secret/connection +-- refs and the SecretBackedMap headers/query_params out of +-- mcp_source.config JSON into proper columns / child tables. pg port +-- of apps/local/drizzle/0009_normalize_mcp.sql. + +CREATE TABLE "mcp_source_header" ( + "id" text NOT NULL, + "scope_id" text NOT NULL, + "source_id" text NOT NULL, + "name" text NOT NULL, + "kind" text NOT NULL, + "text_value" text, + "secret_id" text, + "secret_prefix" text, + CONSTRAINT "mcp_source_header_scope_id_id_pk" PRIMARY KEY("scope_id","id") +); +--> statement-breakpoint +CREATE INDEX "mcp_source_header_scope_id_idx" ON "mcp_source_header" USING btree ("scope_id");--> statement-breakpoint +CREATE INDEX "mcp_source_header_source_id_idx" ON "mcp_source_header" USING btree ("source_id");--> statement-breakpoint +CREATE INDEX "mcp_source_header_secret_id_idx" ON "mcp_source_header" USING btree ("secret_id");--> statement-breakpoint + +CREATE TABLE "mcp_source_query_param" ( + "id" text NOT NULL, + "scope_id" text NOT NULL, + "source_id" text NOT NULL, + "name" text NOT NULL, + "kind" text NOT NULL, + "text_value" text, + "secret_id" text, + "secret_prefix" text, + CONSTRAINT "mcp_source_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") +); +--> statement-breakpoint +CREATE INDEX "mcp_source_query_param_scope_id_idx" ON "mcp_source_query_param" USING btree ("scope_id");--> statement-breakpoint +CREATE INDEX "mcp_source_query_param_source_id_idx" ON "mcp_source_query_param" USING btree ("source_id");--> statement-breakpoint +CREATE INDEX "mcp_source_query_param_secret_id_idx" ON "mcp_source_query_param" USING btree ("secret_id");--> statement-breakpoint + +ALTER TABLE "mcp_source" ADD COLUMN "auth_kind" text DEFAULT 'none' NOT NULL;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_header_name" text;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_secret_id" text;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_secret_prefix" text;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_connection_id" text;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_client_id_secret_id" text;--> statement-breakpoint +ALTER TABLE "mcp_source" ADD COLUMN "auth_client_secret_secret_id" text;--> statement-breakpoint +CREATE INDEX "mcp_source_auth_secret_id_idx" ON "mcp_source" USING btree ("auth_secret_id");--> statement-breakpoint +CREATE INDEX "mcp_source_auth_connection_id_idx" ON "mcp_source" USING btree ("auth_connection_id");--> statement-breakpoint +CREATE INDEX "mcp_source_auth_client_id_secret_id_idx" ON "mcp_source" USING btree ("auth_client_id_secret_id");--> statement-breakpoint +CREATE INDEX "mcp_source_auth_client_secret_secret_id_idx" ON "mcp_source" USING btree ("auth_client_secret_secret_id");--> statement-breakpoint + +-- Only update rows with explicitly current-shape auth (kind=header w/ +-- secretId, or kind=oauth2 w/ connectionId). Legacy inline-OAuth rows +-- are left untouched so the post-migrate migrateLegacyConnections +-- script can convert them to a Connection. +UPDATE "mcp_source" +SET + "auth_kind" = "config"#>>'{auth,kind}', + "auth_header_name" = "config"#>>'{auth,headerName}', + "auth_secret_id" = "config"#>>'{auth,secretId}', + "auth_secret_prefix" = "config"#>>'{auth,prefix}', + "auth_connection_id" = "config"#>>'{auth,connectionId}', + "auth_client_id_secret_id" = "config"#>>'{auth,clientIdSecretId}', + "auth_client_secret_secret_id" = "config"#>>'{auth,clientSecretSecretId}' +WHERE "config" IS NOT NULL + AND ( + ( + "config"#>>'{auth,kind}' = 'header' + AND "config"#>>'{auth,secretId}' IS NOT NULL + ) + OR ( + "config"#>>'{auth,kind}' = 'oauth2' + AND "config"#>>'{auth,connectionId}' IS NOT NULL + ) + );--> statement-breakpoint + +INSERT INTO "mcp_source_header" + ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") +SELECT + s."scope_id", + '[' || to_jsonb(s."id")::text || ',' || to_jsonb(h.key)::text || ']', + s."id", + h.key, + CASE + WHEN jsonb_typeof(h.value) = 'object' AND h.value ? 'secretId' THEN 'secret' + ELSE 'text' + END, + CASE WHEN jsonb_typeof(h.value) = 'string' THEN h.value #>> '{}' ELSE NULL END, + CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'secretId' ELSE NULL END, + CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'prefix' ELSE NULL END +FROM "mcp_source" s, jsonb_each(s."config"->'headers') h +WHERE s."config"->'headers' IS NOT NULL +ON CONFLICT DO NOTHING;--> statement-breakpoint + +INSERT INTO "mcp_source_query_param" + ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") +SELECT + s."scope_id", + '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', + s."id", + q.key, + CASE + WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' + ELSE 'text' + END, + CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END +FROM "mcp_source" s, jsonb_each(s."config"->'queryParams') q +WHERE s."config"->'queryParams' IS NOT NULL +ON CONFLICT DO NOTHING;--> statement-breakpoint + +-- Strip already-copied fields from config JSON. headers/queryParams +-- are always safe; auth is only stripped on rows whose auth was the +-- current shape (legacy inline-OAuth rows keep config.auth so +-- migrateLegacyConnections can mint a Connection from it). +UPDATE "mcp_source" +SET "config" = "config" - 'headers' - 'queryParams' +WHERE "config" IS NOT NULL;--> statement-breakpoint + +UPDATE "mcp_source" +SET "config" = "config" - 'auth' +WHERE "config" IS NOT NULL + AND ( + "config"#>>'{auth,kind}' = 'none' + OR ( + "config"#>>'{auth,kind}' = 'header' + AND "config"#>>'{auth,secretId}' IS NOT NULL + ) + OR ( + "config"#>>'{auth,kind}' = 'oauth2' + AND "config"#>>'{auth,connectionId}' IS NOT NULL + ) + ); diff --git a/apps/cloud/drizzle/meta/0010_snapshot.json b/apps/cloud/drizzle/meta/0010_snapshot.json new file mode 100644 index 000000000..a1b45f6e3 --- /dev/null +++ b/apps/cloud/drizzle/meta/0010_snapshot.json @@ -0,0 +1,2594 @@ +{ + "id": "b8d89563-58e1-4e6b-8674-f502338978e2", + "prevId": "8bfee8ad-2ac4-4d42-bff7-3330052ce94c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": [ + "account_id", + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "name": "blob_namespace_key_pk", + "columns": [ + "namespace", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "name": "connection_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "name": "definition_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_operation": { + "name": "graphql_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "name": "graphql_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source": { + "name": "graphql_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "none" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_auth_connection_id_idx": { + "name": "graphql_source_auth_connection_id_idx", + "columns": [ + { + "expression": "auth_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "name": "graphql_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_binding": { + "name": "mcp_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "name": "mcp_binding_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_source": { + "name": "mcp_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "auth_header_name": { + "name": "auth_header_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_secret_id": { + "name": "auth_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_secret_prefix": { + "name": "auth_secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_client_id_secret_id": { + "name": "auth_client_id_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_client_secret_secret_id": { + "name": "auth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_auth_secret_id_idx": { + "name": "mcp_source_auth_secret_id_idx", + "columns": [ + { + "expression": "auth_secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_auth_connection_id_idx": { + "name": "mcp_source_auth_connection_id_idx", + "columns": [ + { + "expression": "auth_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_auth_client_id_secret_id_idx": { + "name": "mcp_source_auth_client_id_secret_id_idx", + "columns": [ + { + "expression": "auth_client_id_secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_auth_client_secret_secret_id_idx": { + "name": "mcp_source_auth_client_secret_secret_id_idx", + "columns": [ + { + "expression": "auth_client_secret_secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "name": "mcp_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth2_session": { + "name": "oauth2_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "name": "oauth2_session_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_operation": { + "name": "openapi_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "name": "openapi_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source": { + "name": "openapi_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "oauth2": { + "name": "oauth2", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "name": "openapi_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_binding": { + "name": "openapi_source_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + { + "expression": "source_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + { + "expression": "target_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + { + "expression": "slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_secret_id_idx": { + "name": "openapi_source_binding_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_connection_id_idx": { + "name": "openapi_source_binding_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret": { + "name": "secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + { + "expression": "owned_by_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "name": "secret_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source": { + "name": "source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "name": "source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "name": "tool_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "name": "tool_policy_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workos_vault_metadata": { + "name": "workos_vault_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workos_vault_metadata_scope_id_idx": { + "name": "workos_vault_metadata_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workos_vault_metadata_scope_id_id_pk": { + "name": "workos_vault_metadata_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source_header": { + "name": "graphql_source_header", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_header_scope_id_idx": { + "name": "graphql_source_header_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_header_source_id_idx": { + "name": "graphql_source_header_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_header_secret_id_idx": { + "name": "graphql_source_header_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_header_scope_id_id_pk": { + "name": "graphql_source_header_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source_query_param": { + "name": "graphql_source_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_query_param_scope_id_idx": { + "name": "graphql_source_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_query_param_source_id_idx": { + "name": "graphql_source_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_query_param_secret_id_idx": { + "name": "graphql_source_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_query_param_scope_id_id_pk": { + "name": "graphql_source_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_query_param": { + "name": "openapi_source_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_query_param_scope_id_idx": { + "name": "openapi_source_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_query_param_source_id_idx": { + "name": "openapi_source_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_query_param_secret_id_idx": { + "name": "openapi_source_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_query_param_scope_id_id_pk": { + "name": "openapi_source_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_spec_fetch_header": { + "name": "openapi_source_spec_fetch_header", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_spec_fetch_header_scope_id_idx": { + "name": "openapi_source_spec_fetch_header_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_header_source_id_idx": { + "name": "openapi_source_spec_fetch_header_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_header_secret_id_idx": { + "name": "openapi_source_spec_fetch_header_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_header_scope_id_id_pk": { + "name": "openapi_source_spec_fetch_header_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_spec_fetch_query_param": { + "name": "openapi_source_spec_fetch_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_spec_fetch_query_param_scope_id_idx": { + "name": "openapi_source_spec_fetch_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_query_param_source_id_idx": { + "name": "openapi_source_spec_fetch_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_query_param_secret_id_idx": { + "name": "openapi_source_spec_fetch_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_query_param_scope_id_id_pk": { + "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_source_header": { + "name": "mcp_source_header", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcp_source_header_scope_id_idx": { + "name": "mcp_source_header_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_header_source_id_idx": { + "name": "mcp_source_header_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_header_secret_id_idx": { + "name": "mcp_source_header_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_header_scope_id_id_pk": { + "name": "mcp_source_header_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_source_query_param": { + "name": "mcp_source_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcp_source_query_param_scope_id_idx": { + "name": "mcp_source_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_query_param_source_id_idx": { + "name": "mcp_source_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_source_query_param_secret_id_idx": { + "name": "mcp_source_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_query_param_scope_id_id_pk": { + "name": "mcp_source_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index 4e717cfda..75f3b52bc 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1778004434001, "tag": "0009_normalize_openapi", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1778004547002, + "tag": "0010_normalize_mcp", + "breakpoints": true } ] } diff --git a/apps/cloud/src/services/executor-schema.ts b/apps/cloud/src/services/executor-schema.ts index 1b5d1626d..5123e54d4 100644 --- a/apps/cloud/src/services/executor-schema.ts +++ b/apps/cloud/src/services/executor-schema.ts @@ -213,10 +213,53 @@ export const mcp_source = pgTable("mcp_source", { scope_id: text('scope_id').notNull(), name: text('name').notNull(), config: jsonb('config').notNull(), + auth_kind: text('auth_kind', { enum: ['none', 'header', 'oauth2'] }).default("none").notNull(), + auth_header_name: text('auth_header_name'), + auth_secret_id: text('auth_secret_id'), + auth_secret_prefix: text('auth_secret_prefix'), + auth_connection_id: text('auth_connection_id'), + auth_client_id_secret_id: text('auth_client_id_secret_id'), + auth_client_secret_secret_id: text('auth_client_secret_secret_id'), created_at: timestamp('created_at').notNull() }, (table) => [ primaryKey({ columns: [table.scope_id, table.id] }), index("mcp_source_scope_id_idx").on(table.scope_id), + index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id), + index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id), + index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), + index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), +]); + +export const mcp_source_header = pgTable("mcp_source_header", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text('kind', { enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("mcp_source_header_scope_id_idx").on(table.scope_id), + index("mcp_source_header_source_id_idx").on(table.source_id), + index("mcp_source_header_secret_id_idx").on(table.secret_id), +]); + +export const mcp_source_query_param = pgTable("mcp_source_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text('kind', { enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("mcp_source_query_param_scope_id_idx").on(table.scope_id), + index("mcp_source_query_param_source_id_idx").on(table.source_id), + index("mcp_source_query_param_secret_id_idx").on(table.secret_id), ]); export const mcp_binding = pgTable("mcp_binding", { diff --git a/apps/local/drizzle/0009_normalize_mcp.sql b/apps/local/drizzle/0009_normalize_mcp.sql new file mode 100644 index 000000000..9b2ac0f42 --- /dev/null +++ b/apps/local/drizzle/0009_normalize_mcp.sql @@ -0,0 +1,161 @@ +-- Normalize mcp plugin: lift the McpConnectionAuth secret/connection +-- refs and the SecretBackedMap headers/query_params out of +-- mcp_source.config JSON into proper columns / child tables. +-- +-- Old shape: +-- mcp_source.config (json) — McpStoredSourceData discriminated union +-- remote: { transport, endpoint, remoteTransport?, queryParams?, +-- headers?, auth: McpConnectionAuth } +-- stdio: { transport, command, args?, env?, cwd? } +-- +-- New shape: +-- mcp_source gains: auth_kind enum, auth_header_name, auth_secret_id, +-- auth_secret_prefix, auth_connection_id, auth_client_id_secret_id, +-- auth_client_secret_secret_id. The remaining structural fields +-- stay in `config` as JSON because they're plugin-private and +-- vary by transport. +-- mcp_source_header / mcp_source_query_param: child tables for +-- remote sources' SecretBackedMap entries (same column shape as +-- graphql_source_header / openapi_source_query_param). + +CREATE TABLE `mcp_source_header` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `mcp_source_header_scope_id_idx` ON `mcp_source_header` (`scope_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_header_source_id_idx` ON `mcp_source_header` (`source_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_header_secret_id_idx` ON `mcp_source_header` (`secret_id`);--> statement-breakpoint + +CREATE TABLE `mcp_source_query_param` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `mcp_source_query_param_scope_id_idx` ON `mcp_source_query_param` (`scope_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_query_param_source_id_idx` ON `mcp_source_query_param` (`source_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_query_param_secret_id_idx` ON `mcp_source_query_param` (`secret_id`);--> statement-breakpoint + +-- New auth columns. `auth_kind` defaults to "none" so the ALTER passes +-- on existing rows; the backfill below stamps the real value. +ALTER TABLE `mcp_source` ADD `auth_kind` text DEFAULT 'none' NOT NULL;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_header_name` text;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_secret_id` text;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_secret_prefix` text;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_connection_id` text;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_client_id_secret_id` text;--> statement-breakpoint +ALTER TABLE `mcp_source` ADD `auth_client_secret_secret_id` text;--> statement-breakpoint +CREATE INDEX `mcp_source_auth_secret_id_idx` ON `mcp_source` (`auth_secret_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_auth_connection_id_idx` ON `mcp_source` (`auth_connection_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_auth_client_id_secret_id_idx` ON `mcp_source` (`auth_client_id_secret_id`);--> statement-breakpoint +CREATE INDEX `mcp_source_auth_client_secret_secret_id_idx` ON `mcp_source` (`auth_client_secret_secret_id`);--> statement-breakpoint + +-- Backfill auth columns from config.auth — but only for rows whose +-- config.auth matches the *current* shape: +-- - kind=none (no extra fields) +-- - kind=header (secretId present) +-- - kind=oauth2 (connectionId present) +-- Truly-legacy rows (inline OAuth shape with accessTokenSecretId etc.) +-- are left untouched here so the post-migrate `migrateLegacyConnections` +-- script can convert them to a Connection and write the resulting +-- pointer to these columns. Setting auth_kind explicitly to NULL/none +-- on those rows would lose the legacy payload before it gets converted. +UPDATE `mcp_source` +SET + `auth_kind` = json_extract(`config`, '$.auth.kind'), + `auth_header_name` = json_extract(`config`, '$.auth.headerName'), + `auth_secret_id` = json_extract(`config`, '$.auth.secretId'), + `auth_secret_prefix` = json_extract(`config`, '$.auth.prefix'), + `auth_connection_id` = json_extract(`config`, '$.auth.connectionId'), + `auth_client_id_secret_id` = json_extract(`config`, '$.auth.clientIdSecretId'), + `auth_client_secret_secret_id` = json_extract(`config`, '$.auth.clientSecretSecretId') +WHERE `config` IS NOT NULL + AND ( + -- kind=none and "no auth at all" both leave auth_kind defaulted to + -- 'none' (the column DEFAULT), so we only UPDATE rows that have a + -- non-trivial current-shape auth payload to extract. + ( + json_extract(`config`, '$.auth.kind') = 'header' + AND json_extract(`config`, '$.auth.secretId') IS NOT NULL + ) + OR ( + json_extract(`config`, '$.auth.kind') = 'oauth2' + AND json_extract(`config`, '$.auth.connectionId') IS NOT NULL + ) + );--> statement-breakpoint + +-- Backfill mcp_source_header from config.headers. Remote sources only; +-- stdio's config has no `.headers` key so json_each returns nothing. +INSERT OR IGNORE INTO `mcp_source_header` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + json_array(s.`id`, h.`key`), + s.`id`, + h.`key`, + CASE + WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END +FROM `mcp_source` s, json_each(json_extract(s.`config`, '$.headers')) h +WHERE json_extract(s.`config`, '$.headers') IS NOT NULL;--> statement-breakpoint + +INSERT OR IGNORE INTO `mcp_source_query_param` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + json_array(s.`id`, q.`key`), + s.`id`, + q.`key`, + CASE + WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END +FROM `mcp_source` s, json_each(json_extract(s.`config`, '$.queryParams')) q +WHERE json_extract(s.`config`, '$.queryParams') IS NOT NULL;--> statement-breakpoint + +-- Strip the now-extracted fields from the legacy config JSON. Skip +-- rows whose config.auth still holds a legacy inline-OAuth payload — +-- migrateLegacyConnections needs to read it to mint the matching +-- Connection. headers/queryParams are always safe to strip (already +-- copied to child tables). SQLite's json_remove returns the input +-- unchanged when a path is missing, so stdio rows pass through +-- cleanly. +UPDATE `mcp_source` +SET `config` = json_remove(`config`, '$.headers', '$.queryParams') +WHERE `config` IS NOT NULL;--> statement-breakpoint + +UPDATE `mcp_source` +SET `config` = json_remove(`config`, '$.auth') +WHERE `config` IS NOT NULL + AND ( + json_extract(`config`, '$.auth.kind') = 'none' + OR ( + json_extract(`config`, '$.auth.kind') = 'header' + AND json_extract(`config`, '$.auth.secretId') IS NOT NULL + ) + OR ( + json_extract(`config`, '$.auth.kind') = 'oauth2' + AND json_extract(`config`, '$.auth.connectionId') IS NOT NULL + ) + ); diff --git a/apps/local/drizzle/meta/0009_snapshot.json b/apps/local/drizzle/meta/0009_snapshot.json new file mode 100644 index 000000000..b9bc99ad6 --- /dev/null +++ b/apps/local/drizzle/meta/0009_snapshot.json @@ -0,0 +1,2173 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "33333333-4444-5555-6666-777777777777", + "prevId": "22222222-3333-4444-5555-666666666666", + "tables": { + "blob": { + "name": "blob", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "columns": [ + "namespace", + "key" + ], + "name": "blob_namespace_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "connection": { + "name": "connection", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_state": { + "name": "provider_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "connection_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "definition": { + "name": "definition", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "definition_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_binding": { + "name": "google_discovery_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_binding_scope_id_idx": { + "name": "google_discovery_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_binding_source_id_idx": { + "name": "google_discovery_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_source": { + "name": "google_discovery_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_source_scope_id_idx": { + "name": "google_discovery_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_operation": { + "name": "graphql_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source": { + "name": "graphql_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_auth_connection_id_idx": { + "name": "graphql_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_binding": { + "name": "mcp_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source": { + "name": "mcp_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_header_name": { + "name": "auth_header_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_secret_id": { + "name": "auth_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_secret_prefix": { + "name": "auth_secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_id_secret_id": { + "name": "auth_client_id_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_client_secret_secret_id": { + "name": "auth_client_secret_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_auth_secret_id_idx": { + "name": "mcp_source_auth_secret_id_idx", + "columns": [ + "auth_secret_id" + ], + "isUnique": false + }, + "mcp_source_auth_connection_id_idx": { + "name": "mcp_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + }, + "mcp_source_auth_client_id_secret_id_idx": { + "name": "mcp_source_auth_client_id_secret_id_idx", + "columns": [ + "auth_client_id_secret_id" + ], + "isUnique": false + }, + "mcp_source_auth_client_secret_secret_id_idx": { + "name": "mcp_source_auth_client_secret_secret_id_idx", + "columns": [ + "auth_client_secret_secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth2_session": { + "name": "oauth2_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "oauth2_session_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_operation": { + "name": "openapi_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source": { + "name": "openapi_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth2": { + "name": "oauth2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_binding": { + "name": "openapi_source_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + "source_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + "target_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + "slot" + ], + "isUnique": false + }, + "openapi_source_binding_secret_id_idx": { + "name": "openapi_source_binding_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + }, + "openapi_source_binding_connection_id_idx": { + "name": "openapi_source_binding_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret": { + "name": "secret", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + "owned_by_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "secret_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "source": { + "name": "source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "can_remove": { + "name": "can_remove", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool": { + "name": "tool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_schema": { + "name": "input_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool_policy": { + "name": "tool_policy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + "scope_id", + "position" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_policy_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_header": { + "name": "graphql_source_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_header_scope_id_idx": { + "name": "graphql_source_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_header_source_id_idx": { + "name": "graphql_source_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_header_secret_id_idx": { + "name": "graphql_source_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_query_param": { + "name": "graphql_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_query_param_scope_id_idx": { + "name": "graphql_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_query_param_source_id_idx": { + "name": "graphql_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_query_param_secret_id_idx": { + "name": "graphql_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_query_param": { + "name": "openapi_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_query_param_scope_id_idx": { + "name": "openapi_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_query_param_source_id_idx": { + "name": "openapi_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_query_param_secret_id_idx": { + "name": "openapi_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_header": { + "name": "openapi_source_spec_fetch_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_header_scope_id_idx": { + "name": "openapi_source_spec_fetch_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_source_id_idx": { + "name": "openapi_source_spec_fetch_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_secret_id_idx": { + "name": "openapi_source_spec_fetch_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_query_param": { + "name": "openapi_source_spec_fetch_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_query_param_scope_id_idx": { + "name": "openapi_source_spec_fetch_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_source_id_idx": { + "name": "openapi_source_spec_fetch_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_secret_id_idx": { + "name": "openapi_source_spec_fetch_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source_header": { + "name": "mcp_source_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_header_scope_id_idx": { + "name": "mcp_source_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_header_source_id_idx": { + "name": "mcp_source_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "mcp_source_header_secret_id_idx": { + "name": "mcp_source_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source_query_param": { + "name": "mcp_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_query_param_scope_id_idx": { + "name": "mcp_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_source_query_param_source_id_idx": { + "name": "mcp_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "mcp_source_query_param_secret_id_idx": { + "name": "mcp_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/local/drizzle/meta/_journal.json b/apps/local/drizzle/meta/_journal.json index b77702d02..704c6104f 100644 --- a/apps/local/drizzle/meta/_journal.json +++ b/apps/local/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1778100000001, "tag": "0008_normalize_openapi", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1778100000002, + "tag": "0009_normalize_mcp", + "breakpoints": true } ] } diff --git a/apps/local/src/server/executor-schema.ts b/apps/local/src/server/executor-schema.ts index fa05adfb7..22a91d468 100644 --- a/apps/local/src/server/executor-schema.ts +++ b/apps/local/src/server/executor-schema.ts @@ -213,10 +213,53 @@ export const mcp_source = sqliteTable("mcp_source", { scope_id: text('scope_id').notNull(), name: text('name').notNull(), config: text('config', { mode: "json" }).notNull(), + auth_kind: text({ enum: ['none', 'header', 'oauth2'] }).default("none").notNull(), + auth_header_name: text('auth_header_name'), + auth_secret_id: text('auth_secret_id'), + auth_secret_prefix: text('auth_secret_prefix'), + auth_connection_id: text('auth_connection_id'), + auth_client_id_secret_id: text('auth_client_id_secret_id'), + auth_client_secret_secret_id: text('auth_client_secret_secret_id'), created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull() }, (table) => [ primaryKey({ columns: [table.scope_id, table.id] }), index("mcp_source_scope_id_idx").on(table.scope_id), + index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id), + index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id), + index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), + index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), +]); + +export const mcp_source_header = sqliteTable("mcp_source_header", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("mcp_source_header_scope_id_idx").on(table.scope_id), + index("mcp_source_header_source_id_idx").on(table.source_id), + index("mcp_source_header_secret_id_idx").on(table.secret_id), +]); + +export const mcp_source_query_param = sqliteTable("mcp_source_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("mcp_source_query_param_scope_id_idx").on(table.scope_id), + index("mcp_source_query_param_source_id_idx").on(table.source_id), + index("mcp_source_query_param_secret_id_idx").on(table.secret_id), ]); export const mcp_binding = sqliteTable("mcp_binding", { diff --git a/apps/local/src/server/migrate-connections.test.ts b/apps/local/src/server/migrate-connections.test.ts index b88c68780..b2d2a75eb 100644 --- a/apps/local/src/server/migrate-connections.test.ts +++ b/apps/local/src/server/migrate-connections.test.ts @@ -86,13 +86,21 @@ describe("migrateLegacyConnections", () => { refresh_token_secret_id: "refresh-token", }); + // Post-0009 the canonical auth lives in dedicated columns. The + // legacy migrator strips config.auth and writes the new pointer to + // auth_kind / auth_connection_id directly. const source = db - .prepare("SELECT config FROM mcp_source WHERE scope_id = ? AND id = ?") - .get("scope-1", "remote-mcp") as { readonly config: string }; - expect(JSON.parse(source.config).auth).toEqual({ - kind: "oauth2", - connectionId: "mcp-oauth2-remote-mcp", - }); + .prepare( + "SELECT config, auth_kind, auth_connection_id FROM mcp_source WHERE scope_id = ? AND id = ?", + ) + .get("scope-1", "remote-mcp") as { + readonly config: string; + readonly auth_kind: string; + readonly auth_connection_id: string; + }; + expect(JSON.parse(source.config).auth).toBeUndefined(); + expect(source.auth_kind).toBe("oauth2"); + expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp"); const ownedSecrets = db .prepare( diff --git a/apps/local/src/server/migrate-connections.ts b/apps/local/src/server/migrate-connections.ts index ecd17bdfd..b680f7424 100644 --- a/apps/local/src/server/migrate-connections.ts +++ b/apps/local/src/server/migrate-connections.ts @@ -412,9 +412,23 @@ const migrateMcp = (sqlite: Database): void => { .all() as ReadonlyArray; if (rows.length === 0) return; - const updateSource = sqlite.prepare( + // Post-0009 mcp_source has dedicated auth_kind / auth_connection_id + // columns. The auth ETL inside drizzle migration 0009 only fires for + // rows whose config.auth carries `connectionId` — i.e., already + // post-Connection shape. Truly-legacy rows (inline OAuth shape with + // accessTokenSecretId) survive unchanged and fall to this script. + // Detect them, mint a Connection, rewire owned secrets, then write + // the result to the new columns (not back into config.auth, which + // 0009 stripped). + const hasAuthColumns = columnExists(sqlite, "mcp_source", "auth_kind"); + const updateConfig = sqlite.prepare( "UPDATE mcp_source SET config = ? WHERE scope_id = ? AND id = ?", ); + const updateConfigAndAuth = hasAuthColumns + ? sqlite.prepare( + "UPDATE mcp_source SET config = ?, auth_kind = 'oauth2', auth_connection_id = ? WHERE scope_id = ? AND id = ?", + ) + : null; for (const row of rows) { let config: Record = {}; @@ -452,8 +466,14 @@ const migrateMcp = (sqlite: Database): void => { resourceMetadataUrl: legacy.resourceMetadataUrl, resourceMetadata: null, }; - const authPointer = { kind: "oauth2" as const, connectionId }; - const nextConfig = { ...config, auth: authPointer }; + // Strip auth from config — post-0009 the canonical home is the + // auth_* columns. Pre-0009 we still write the pointer back into + // config.auth so the older code path keeps working. + const { auth: _unused, ...configWithoutAuth } = config; + void _unused; + const nextConfig = hasAuthColumns + ? configWithoutAuth + : { ...config, auth: { kind: "oauth2" as const, connectionId } }; const secretIds = [legacy.accessTokenSecretId]; const secretNames = [`Connection ${connectionId} access token`]; @@ -476,7 +496,16 @@ const migrateMcp = (sqlite: Database): void => { }); const err = rewireSecrets(sqlite, row.scope_id, connectionId, secretIds, secretNames); if (err) throw new Error(err); - updateSource.run(JSON.stringify(nextConfig), row.scope_id, row.id); + if (updateConfigAndAuth) { + updateConfigAndAuth.run( + JSON.stringify(nextConfig), + connectionId, + row.scope_id, + row.id, + ); + } else { + updateConfig.run(JSON.stringify(nextConfig), row.scope_id, row.id); + } }); try { txn(); diff --git a/apps/local/src/server/migrate-graphql-bindings.test.ts b/apps/local/src/server/migrate-graphql-bindings.test.ts index b244dbbb2..5cb1e982d 100644 --- a/apps/local/src/server/migrate-graphql-bindings.test.ts +++ b/apps/local/src/server/migrate-graphql-bindings.test.ts @@ -69,6 +69,24 @@ const PRE_0007_SQL = ` created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); + + CREATE TABLE mcp_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE mcp_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); `; // drizzle's sqlite migrator picks the latest `created_at` from diff --git a/apps/local/src/server/migrate-mcp-bindings.test.ts b/apps/local/src/server/migrate-mcp-bindings.test.ts new file mode 100644 index 000000000..ac8c360e2 --- /dev/null +++ b/apps/local/src/server/migrate-mcp-bindings.test.ts @@ -0,0 +1,234 @@ +// End-to-end test for `0009_normalize_mcp.sql`. Seeds an mcp_source +// row with the legacy json shape (config containing auth/headers/ +// queryParams), runs the migration runner, asserts the auth columns +// are populated and the child tables hold the secret-backed entries. + +import { describe, expect, it } from "@effect/vitest"; +import { Database } from "bun:sqlite"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); + +const PRE_0009_SQL = ` + CREATE TABLE __drizzle_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT NOT NULL, + created_at NUMERIC + ); + + CREATE TABLE mcp_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE mcp_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); +`; + +const STAMP_BEFORE = 1778100000001; // 0008_normalize_openapi.when + +const stampPriorMigrationsApplied = (db: Database) => { + db.prepare( + "INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)", + ).run("pre-0009-marker", STAMP_BEFORE); +}; + +describe("0009_normalize_mcp backfill", () => { + it("flattens header auth into auth_kind/auth_secret_id columns", () => { + const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0009_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", + ).run( + "default-scope", + "remote-headers", + "Remote Headers", + JSON.stringify({ + transport: "remote", + endpoint: "https://example.com/mcp", + auth: { + kind: "header", + headerName: "X-API-Key", + secretId: "tok-secret", + prefix: "Bearer ", + }, + }), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const row = after + .prepare( + "SELECT auth_kind, auth_header_name, auth_secret_id, auth_secret_prefix, config FROM mcp_source WHERE id = ?", + ) + .get("remote-headers") as { + auth_kind: string; + auth_header_name: string; + auth_secret_id: string; + auth_secret_prefix: string; + config: string; + }; + expect(row.auth_kind).toBe("header"); + expect(row.auth_header_name).toBe("X-API-Key"); + expect(row.auth_secret_id).toBe("tok-secret"); + expect(row.auth_secret_prefix).toBe("Bearer "); + // The auth key should be stripped from config json after migration. + const config = JSON.parse(row.config); + expect(config.auth).toBeUndefined(); + expect(config.transport).toBe("remote"); + expect(config.endpoint).toBe("https://example.com/mcp"); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("flattens oauth2 auth and explodes headers into child rows", () => { + const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0009_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", + ).run( + "default-scope", + "remote-oauth", + "Remote OAuth", + JSON.stringify({ + transport: "remote", + endpoint: "https://oauth.example/mcp", + headers: { + "X-Trace": "static", + "X-Token": { secretId: "extra-tok" }, + }, + queryParams: { + org: { secretId: "org-id-secret" }, + }, + auth: { + kind: "oauth2", + connectionId: "conn-1", + clientIdSecretId: "client-id-sec", + clientSecretSecretId: "client-secret-sec", + }, + }), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const row = after + .prepare( + "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id FROM mcp_source WHERE id = ?", + ) + .get("remote-oauth") as Record; + expect(row.auth_kind).toBe("oauth2"); + expect(row.auth_connection_id).toBe("conn-1"); + expect(row.auth_client_id_secret_id).toBe("client-id-sec"); + expect(row.auth_client_secret_secret_id).toBe("client-secret-sec"); + + const headers = after + .prepare( + "SELECT name, kind, text_value, secret_id FROM mcp_source_header WHERE source_id = ? ORDER BY name", + ) + .all("remote-oauth") as ReadonlyArray>; + expect(headers).toHaveLength(2); + const byName = new Map(headers.map((h) => [h.name, h])); + expect(byName.get("X-Trace")).toMatchObject({ + kind: "text", + text_value: "static", + }); + expect(byName.get("X-Token")).toMatchObject({ + kind: "secret", + secret_id: "extra-tok", + }); + + const params = after + .prepare( + "SELECT name, secret_id FROM mcp_source_query_param WHERE source_id = ?", + ) + .all("remote-oauth") as ReadonlyArray>; + expect(params).toHaveLength(1); + expect(params[0]).toMatchObject({ name: "org", secret_id: "org-id-secret" }); + + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("leaves stdio sources alone (no auth, no headers, no queryParams)", () => { + const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0009_SQL); + stampPriorMigrationsApplied(db); + + db.prepare( + "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", + ).run( + "default-scope", + "stdio-only", + "Stdio", + JSON.stringify({ + transport: "stdio", + command: "/usr/bin/server", + args: ["--flag"], + }), + Date.now(), + ); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const row = after + .prepare( + "SELECT auth_kind, auth_secret_id, config FROM mcp_source WHERE id = ?", + ) + .get("stdio-only") as { + auth_kind: string; + auth_secret_id: string | null; + config: string; + }; + expect(row.auth_kind).toBe("none"); + expect(row.auth_secret_id).toBeNull(); + const config = JSON.parse(row.config); + expect(config.transport).toBe("stdio"); + expect(config.command).toBe("/usr/bin/server"); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/local/src/server/migrate-openapi-bindings.test.ts b/apps/local/src/server/migrate-openapi-bindings.test.ts index a44ee054b..367a0a7d4 100644 --- a/apps/local/src/server/migrate-openapi-bindings.test.ts +++ b/apps/local/src/server/migrate-openapi-bindings.test.ts @@ -56,6 +56,24 @@ const PRE_0008_SQL = ` created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); + + CREATE TABLE mcp_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE mcp_binding ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (scope_id, id) + ); `; // Stamp 0007's folderMillis from the journal so drizzle's runner skips diff --git a/packages/plugins/mcp/src/sdk/binding-store.ts b/packages/plugins/mcp/src/sdk/binding-store.ts index 266d03683..898340c29 100644 --- a/packages/plugins/mcp/src/sdk/binding-store.ts +++ b/packages/plugins/mcp/src/sdk/binding-store.ts @@ -1,7 +1,19 @@ // --------------------------------------------------------------------------- -// MCP plugin storage — two tables (mcp_source, mcp_binding). OAuth -// session storage lives at the core level in `oauth2_session` and is -// owned by `ctx.oauth`. +// MCP plugin storage — four tables: +// - mcp_source: per-source structural data (transport, endpoint, +// stdio command/args/env, etc.) plus the auth flattened into +// columns so secret/connection refs are queryable. The non-ref +// structural data still lives in `config` as JSON because it's +// plugin-private and varies by transport (`remote` vs `stdio` +// have different shapes). +// - mcp_source_header / mcp_source_query_param: child tables for +// remote sources' headers and query_params SecretBackedMap entries. +// - mcp_binding: per-tool McpToolBinding (toolId/toolName/description/ +// input+output schemas/annotations). Stays JSON: it carries no +// refs, and `inputSchema` / `outputSchema` are arbitrary +// user-supplied JSON Schemas — a legitimate JSON case. +// OAuth session storage lives at the core level in `oauth2_session` +// and is owned by `ctx.oauth`. // --------------------------------------------------------------------------- import { Effect, Schema } from "effect"; @@ -12,7 +24,12 @@ import { type StorageFailure, } from "@executor-js/sdk/core"; -import { McpToolBinding, McpStoredSourceData } from "./types"; +import { + McpToolBinding, + McpStoredSourceData, + type McpConnectionAuth, + type SecretBackedValue, +} from "./types"; // --------------------------------------------------------------------------- // Schema @@ -24,10 +41,61 @@ export const mcpSchema = defineSchema({ id: { type: "string", required: true }, scope_id: { type: "string", required: true, index: true }, name: { type: "string", required: true }, + // Plugin-private structural data minus the ref-bearing fields + // (auth, headers, queryParams). For remote sources: transport, + // endpoint, remoteTransport. For stdio: transport, command, + // args, env, cwd. config: { type: "json", required: true }, + // Flattened McpConnectionAuth. Exactly one of the kind-tagged + // groups is populated for non-`none` auths. + auth_kind: { + type: ["none", "header", "oauth2"], + required: true, + defaultValue: "none", + }, + // Header-auth fields. + auth_header_name: { type: "string", required: false }, + auth_secret_id: { type: "string", required: false, index: true }, + auth_secret_prefix: { type: "string", required: false }, + // OAuth2 auth fields. + auth_connection_id: { type: "string", required: false, index: true }, + auth_client_id_secret_id: { + type: "string", + required: false, + index: true, + }, + auth_client_secret_secret_id: { + type: "string", + required: false, + index: true, + }, created_at: { type: "date", required: true }, }, }, + mcp_source_header: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, + mcp_source_query_param: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, mcp_binding: { fields: { id: { type: "string", required: true }, @@ -61,6 +129,126 @@ const coerceJson = (value: unknown): unknown => { } }; +// --- auth column packing/unpacking ------------------------------------------ + +interface AuthColumns { + readonly auth_kind: "none" | "header" | "oauth2"; + readonly auth_header_name?: string; + readonly auth_secret_id?: string; + readonly auth_secret_prefix?: string; + readonly auth_connection_id?: string; + readonly auth_client_id_secret_id?: string; + readonly auth_client_secret_secret_id?: string; +} + +const authToColumns = (auth: McpConnectionAuth): AuthColumns => { + if (auth.kind === "header") { + return { + auth_kind: "header", + auth_header_name: auth.headerName, + auth_secret_id: auth.secretId, + auth_secret_prefix: auth.prefix, + }; + } + if (auth.kind === "oauth2") { + return { + auth_kind: "oauth2", + auth_connection_id: auth.connectionId, + auth_client_id_secret_id: auth.clientIdSecretId, + auth_client_secret_secret_id: auth.clientSecretSecretId ?? undefined, + }; + } + return { auth_kind: "none" }; +}; + +const columnsToAuth = (row: Record): McpConnectionAuth => { + const kind = row.auth_kind as string; + if (kind === "header" && typeof row.auth_secret_id === "string") { + const prefix = row.auth_secret_prefix as string | null | undefined; + return { + kind: "header", + headerName: (row.auth_header_name as string | null) ?? "", + secretId: row.auth_secret_id, + ...(prefix ? { prefix } : {}), + }; + } + if (kind === "oauth2" && typeof row.auth_connection_id === "string") { + const cid = row.auth_client_id_secret_id as string | null | undefined; + const csec = row.auth_client_secret_secret_id as string | null | undefined; + return { + kind: "oauth2", + connectionId: row.auth_connection_id, + ...(cid ? { clientIdSecretId: cid } : {}), + ...(csec !== undefined && csec !== null + ? { clientSecretSecretId: csec } + : {}), + }; + } + return { kind: "none" }; +}; + +// --- SecretBackedMap <-> child rows (mcp_source_header / query_param) ------- + +interface SecretBackedRow { + readonly id: string; + readonly scope_id: string; + readonly source_id: string; + readonly name: string; + readonly kind: "text" | "secret"; + readonly text_value?: string; + readonly secret_id?: string; + readonly secret_prefix?: string; + readonly [k: string]: unknown; +} + +const valueMapToRows = ( + sourceId: string, + scope: string, + values: Record | undefined, +): readonly SecretBackedRow[] => { + if (!values) return []; + return Object.entries(values).map(([name, value]) => { + const id = JSON.stringify([sourceId, name]); + if (typeof value === "string") { + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "text", + text_value: value, + }; + } + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "secret", + secret_id: value.secretId, + secret_prefix: value.prefix, + }; + }); +}; + +const rowsToValueMap = ( + rows: readonly Record[], +): Record => { + const out: Record = {}; + for (const row of rows) { + const name = row.name as string; + if (row.kind === "secret" && typeof row.secret_id === "string") { + const prefix = row.secret_prefix as string | undefined | null; + out[name] = prefix + ? { secretId: row.secret_id, prefix } + : { secretId: row.secret_id }; + } else if (row.kind === "text" && typeof row.text_value === "string") { + out[name] = row.text_value; + } + } + return out; +}; + // --------------------------------------------------------------------------- // Stored source (decoded) — what callers see // --------------------------------------------------------------------------- @@ -136,6 +324,55 @@ export interface McpBindingStore { namespace: string, scope: string, ) => Effect.Effect; + + // --------------------------------------------------------------------- + // Usage lookups — back `usagesForSecret` / `usagesForConnection`. + // --------------------------------------------------------------------- + + /** Source rows whose flattened auth columns reference the given + * secret id. The `slot` field on each result tags which column + * matched so the caller can produce a precise Usage.slot. */ + readonly findSourcesBySecret: ( + secretId: string, + ) => Effect.Effect< + readonly { + readonly namespace: string; + readonly scope_id: string; + readonly name: string; + readonly slot: string; + }[], + StorageFailure + >; + + /** Source rows whose oauth2 auth points at the given connection id. */ + readonly findSourcesByConnection: ( + connectionId: string, + ) => Effect.Effect< + readonly { + readonly namespace: string; + readonly scope_id: string; + readonly name: string; + readonly slot: string; + }[], + StorageFailure + >; + + /** Header / query_param child rows that reference the given secret id. */ + readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< + readonly { + readonly kind: "header" | "query_param"; + readonly source_id: string; + readonly scope_id: string; + readonly name: string; + }[], + StorageFailure + >; + + /** Resolve display names for `(scope_id, source_id)` pairs in one + * round trip. Keys: `${scope_id}:${source_id}`. */ + readonly lookupSourceNames: ( + keys: readonly string[], + ) => Effect.Effect, StorageFailure>; } // --------------------------------------------------------------------------- @@ -217,7 +454,7 @@ export const makeMcpStore = ({ namespace: row.id, scope: row.scope_id, name: row.name, - config: decodeSourceData(coerceJson(row.config)), + config: yield* hydrateSourceData(row, namespace, scope), }; }), @@ -231,12 +468,14 @@ export const makeMcpStore = ({ ], }); if (!row) return null; - return decodeSourceData(coerceJson(row.config)); + return yield* hydrateSourceData(row, namespace, scope); }), putSource: (source) => Effect.gen(function* () { const now = new Date(); + // Drop the source row and its child rows; recreate. Two-step + // matches the existing put-overwrites-existing semantic. yield* db.delete({ model: "mcp_source", where: [ @@ -244,17 +483,67 @@ export const makeMcpStore = ({ { field: "scope_id", value: source.scope }, ], }); + yield* deleteSourceChildren(source.namespace, source.scope); + + const auth: McpConnectionAuth = + source.config.transport === "remote" + ? source.config.auth + : { kind: "none" }; + const authCols = authToColumns(auth); + const headers = + source.config.transport === "remote" + ? source.config.headers + : undefined; + const queryParams = + source.config.transport === "remote" + ? source.config.queryParams + : undefined; + + // The encoded config keeps every plugin-private field but + // strips auth/headers/queryParams — those moved to columns/ + // child tables. We round-trip through encodeSourceData so the + // remaining fields stay in the same JSON shape decode expects. + const encodedConfig = stripExtractedFields( + encodeSourceData(source.config) as Record, + ); + yield* db.create({ model: "mcp_source", data: { id: source.namespace, scope_id: source.scope, name: source.name, - config: encodeSourceData(source.config), + config: encodedConfig, created_at: now, + ...authCols, }, forceAllowId: true, }); + + const headerRows = valueMapToRows( + source.namespace, + source.scope, + headers, + ); + if (headerRows.length > 0) { + yield* db.createMany({ + model: "mcp_source_header", + data: headerRows, + forceAllowId: true, + }); + } + const paramRows = valueMapToRows( + source.namespace, + source.scope, + queryParams, + ); + if (paramRows.length > 0) { + yield* db.createMany({ + model: "mcp_source_query_param", + data: paramRows, + forceAllowId: true, + }); + } }), removeSource: (namespace, scope) => @@ -266,6 +555,7 @@ export const makeMcpStore = ({ { field: "scope_id", value: scope }, ], }); + yield* deleteSourceChildren(namespace, scope); yield* db.delete({ model: "mcp_source", where: [ @@ -274,5 +564,189 @@ export const makeMcpStore = ({ ], }); }), + + findSourcesBySecret: (secretId) => + Effect.gen(function* () { + // Three places a secret id can land on an mcp_source row: the + // header-auth secret, and the two oauth2 client_*_secret_id + // columns. Run all three lookups in parallel and dedupe by + // (scope_id, id). + const [byHeader, byClientId, byClientSecret] = yield* Effect.all( + [ + db.findMany({ + model: "mcp_source", + where: [{ field: "auth_secret_id", value: secretId }], + }), + db.findMany({ + model: "mcp_source", + where: [ + { field: "auth_client_id_secret_id", value: secretId }, + ], + }), + db.findMany({ + model: "mcp_source", + where: [ + { field: "auth_client_secret_secret_id", value: secretId }, + ], + }), + ], + { concurrency: "unbounded" }, + ); + const dedup = new Map>(); + for (const r of [...byHeader, ...byClientId, ...byClientSecret]) { + dedup.set(`${r.scope_id}:${r.id}`, r); + } + return [...dedup.values()].map((row) => ({ + namespace: row.id as string, + scope_id: row.scope_id as string, + name: row.name as string, + slot: + (byHeader as readonly Record[]).includes(row) + ? "auth.header" + : (byClientId as readonly Record[]).includes( + row, + ) + ? "auth.oauth2.client_id" + : "auth.oauth2.client_secret", + })); + }), + + findSourcesByConnection: (connectionId) => + db + .findMany({ + model: "mcp_source", + where: [{ field: "auth_connection_id", value: connectionId }], + }) + .pipe( + Effect.map((rows) => + rows.map((r) => ({ + namespace: r.id as string, + scope_id: r.scope_id as string, + name: r.name as string, + slot: "auth.oauth2.connection", + })), + ), + ), + + findChildRowsBySecret: (secretId) => + Effect.gen(function* () { + const [headers, params] = yield* Effect.all( + [ + db.findMany({ + model: "mcp_source_header", + where: [{ field: "secret_id", value: secretId }], + }), + db.findMany({ + model: "mcp_source_query_param", + where: [{ field: "secret_id", value: secretId }], + }), + ], + { concurrency: "unbounded" }, + ); + return [ + ...headers.map((r) => ({ + kind: "header" as const, + source_id: r.source_id as string, + scope_id: r.scope_id as string, + name: r.name as string, + })), + ...params.map((r) => ({ + kind: "query_param" as const, + source_id: r.source_id as string, + scope_id: r.scope_id as string, + name: r.name as string, + })), + ]; + }), + + lookupSourceNames: (keys) => + Effect.gen(function* () { + if (keys.length === 0) return new Map(); + const rows = yield* db.findMany({ model: "mcp_source" }); + const requested = new Set(keys); + const out = new Map(); + for (const r of rows) { + const key = `${r.scope_id as string}:${r.id as string}`; + if (requested.has(key)) out.set(key, r.name as string); + } + return out; + }), }; + + // --------------------------------------------------------------------- + // Private helpers — depend on `db` so they live inside the closure. + // --------------------------------------------------------------------- + + function deleteSourceChildren(namespace: string, scope: string) { + return Effect.gen(function* () { + for (const model of [ + "mcp_source_header", + "mcp_source_query_param", + ] as const) { + yield* db.deleteMany({ + model, + where: [ + { field: "source_id", value: namespace }, + { field: "scope_id", value: scope }, + ], + }); + } + }); + } + + function hydrateSourceData( + row: Record, + namespace: string, + scope: string, + ): Effect.Effect { + return Effect.gen(function* () { + // The stored JSON has auth/headers/queryParams stripped (those + // moved to columns / child tables). We must rehydrate the full + // shape BEFORE handing it to the schema decoder, because + // `McpRemoteSourceData.auth` is required. + const partial = coerceJson(row.config) as Record; + if (partial.transport !== "remote") { + // stdio sources have no extracted fields — decode as-is. + return decodeSourceData(partial); + } + const headerRows = yield* db.findMany({ + model: "mcp_source_header", + where: [ + { field: "source_id", value: namespace }, + { field: "scope_id", value: scope }, + ], + }); + const paramRows = yield* db.findMany({ + model: "mcp_source_query_param", + where: [ + { field: "source_id", value: namespace }, + { field: "scope_id", value: scope }, + ], + }); + const headers = rowsToValueMap(headerRows); + const queryParams = rowsToValueMap(paramRows); + const reassembled = { + ...partial, + ...(Object.keys(headers).length > 0 ? { headers } : {}), + ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), + auth: columnsToAuth(row), + }; + return decodeSourceData(reassembled); + }); + } +}; + +// Strip auth/headers/queryParams from the encoded source-data shape. +// Keeps the remaining structural fields (transport, endpoint, etc.) in +// the JSON config column. Per-transport: only the remote variant has +// these fields, so this is a no-op for stdio. +const stripExtractedFields = ( + encoded: Record, +): Record => { + if (encoded.transport !== "remote") return encoded; + const { auth, headers, queryParams, ...rest } = encoded; + void auth; + void headers; + void queryParams; + return rest; }; diff --git a/packages/plugins/mcp/src/sdk/plugin.test.ts b/packages/plugins/mcp/src/sdk/plugin.test.ts index 2640d3f8c..cfd2ead8c 100644 --- a/packages/plugins/mcp/src/sdk/plugin.test.ts +++ b/packages/plugins/mcp/src/sdk/plugin.test.ts @@ -635,6 +635,82 @@ describe("mcpPlugin", () => { ); }), ); + + // ------------------------------------------------------------------------- + // Usage tracking — refs land on auth_* columns + child tables and the + // plugin's `usagesForSecret` / `usagesForConnection` should surface + // every one. addSource against an unreachable endpoint still persists + // the source row so the assertion runs without needing a live server. + // ------------------------------------------------------------------------- + + it.effect("usagesForSecret aggregates header-auth + headers child rows", () => + Effect.gen(function* () { + const executor = yield* createExecutor( + makeTestConfig({ plugins: [mcpPlugin()] as const }), + ); + + yield* executor.mcp + .addSource({ + transport: "remote", + scope: "test-scope", + name: "header-auth", + endpoint: "http://127.0.0.1:1/mcp", + namespace: "header_auth_source", + auth: { + kind: "header", + headerName: "X-API-Key", + secretId: "shared-key", + }, + headers: { "X-Trace": { secretId: "shared-key" } }, + queryParams: { ping: { secretId: "other-secret" } }, + }) + .pipe(Effect.result); + + const usages = yield* executor.secrets.usages( + SecretId.make("shared-key"), + ); + expect(usages.length).toBe(2); + const slots = usages.map((u) => u.slot).sort(); + expect(slots).toEqual(["auth.header", "header:X-Trace"]); + expect(usages.every((u) => u.pluginId === "mcp")).toBe(true); + + const otherUsages = yield* executor.secrets.usages( + SecretId.make("other-secret"), + ); + expect(otherUsages.length).toBe(1); + expect(otherUsages[0].slot).toBe("query_param:ping"); + }), + ); + + it.effect("usagesForConnection finds oauth2-bound mcp sources", () => + Effect.gen(function* () { + const executor = yield* createExecutor( + makeTestConfig({ plugins: [mcpPlugin()] as const }), + ); + + yield* executor.mcp + .addSource({ + transport: "remote", + scope: "test-scope", + name: "oauth-source", + endpoint: "http://127.0.0.1:1/mcp", + namespace: "oauth_ref", + auth: { kind: "oauth2", connectionId: "conn-xyz" }, + }) + .pipe(Effect.result); + + const usages = yield* executor.connections.usages( + ConnectionId.make("conn-xyz"), + ); + expect(usages.length).toBe(1); + expect(usages[0]).toMatchObject({ + pluginId: "mcp", + ownerKind: "mcp-source", + ownerId: "oauth_ref", + slot: "auth.oauth2.connection", + }); + }), + ); }); // --------------------------------------------------------------------------- diff --git a/packages/plugins/mcp/src/sdk/plugin.ts b/packages/plugins/mcp/src/sdk/plugin.ts index ed7d3b811..ab63b345d 100644 --- a/packages/plugins/mcp/src/sdk/plugin.ts +++ b/packages/plugins/mcp/src/sdk/plugin.ts @@ -6,7 +6,9 @@ import { McpGroup } from "../api/group"; import { McpExtensionService, McpHandlers } from "../api/handlers"; import { + ScopeId, SourceDetectionResult, + Usage, definePlugin, resolveSecretBackedMap as resolveSharedSecretBackedMap, type PluginCtx, @@ -987,6 +989,71 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { yield* ctx.storage.removeSource(sourceId, scope); }), + // Aggregate usages across the four places mcp can hold a secret ref: + // header-auth secret, oauth2 client_id_secret, oauth2 client_secret, + // and the per-entry rows in mcp_source_header / mcp_source_query_param. + usagesForSecret: ({ ctx, args }) => + Effect.gen(function* () { + const sources = yield* ctx.storage.findSourcesBySecret(args.secretId); + const childRows = yield* ctx.storage.findChildRowsBySecret( + args.secretId, + ); + + const sourceKeys = new Set(); + for (const s of sources) { + sourceKeys.add(`${s.scope_id}:${s.namespace}`); + } + for (const r of childRows) { + sourceKeys.add(`${r.scope_id}:${r.source_id}`); + } + const names = yield* ctx.storage.lookupSourceNames([...sourceKeys]); + + const out: Usage[] = []; + for (const s of sources) { + out.push( + new Usage({ + pluginId: "mcp", + scopeId: ScopeId.make(s.scope_id), + ownerKind: "mcp-source", + ownerId: s.namespace, + ownerName: names.get(`${s.scope_id}:${s.namespace}`) ?? s.name, + slot: s.slot, + }), + ); + } + for (const r of childRows) { + out.push( + new Usage({ + pluginId: "mcp", + scopeId: ScopeId.make(r.scope_id), + ownerKind: `mcp-source-${r.kind.replace(/_/g, "-")}`, + ownerId: r.source_id, + ownerName: names.get(`${r.scope_id}:${r.source_id}`) ?? null, + slot: `${r.kind}:${r.name}`, + }), + ); + } + return out; + }), + + usagesForConnection: ({ ctx, args }) => + Effect.gen(function* () { + const sources = yield* ctx.storage.findSourcesByConnection( + args.connectionId, + ); + return sources.map( + (s) => + new Usage({ + pluginId: "mcp", + scopeId: ScopeId.make(s.scope_id), + ownerKind: "mcp-source", + ownerId: s.namespace, + ownerName: s.name, + slot: s.slot, + }), + ); + }), + refreshSource: () => Effect.void, // Connection refresh for oauth2-minted sources is owned by the