History

Merge branch 'main' into rs/mcp-per-user-oauth

Resolves migration ordering: main added 0001_harsh_meltdown (source_url) while this branch added 0001_funny_betty_brant (expires_at bigint). Kept main's 0001 as-is and regenerated the bigint migration as 0002. Cloud schema auto-merge picked up both changes.

Rhys Sullivan committed 6 months ago

Commit d34dec82 parents b32b601 596d57615 files · +3263 −90Browse files

Unified · Split

15 files
  1. apps/cloud/drizzle/0001_harsh_meltdown.sql +1 −0
  2. apps/cloud/drizzle/0002_fat_white_tiger.sql +0 −0
  3. apps/cloud/drizzle/meta/0001_snapshot.json +8 −2
  4. apps/cloud/drizzle/meta/0002_snapshot.json +1276 −0
  5. apps/cloud/drizzle/meta/_journal.json +9 −2
  6. apps/cloud/src/services/executor-schema.ts +2 −0
  7. apps/cloud/src/services/sources-refresh.node.test.ts +177 −0
  8. apps/local/drizzle/0001_sour_catseye.sql +1 −0
  9. apps/local/drizzle/meta/0001_snapshot.json +1143 −0
  10. apps/local/drizzle/meta/_journal.json +7 −0
  11. apps/local/src/server/executor-schema.ts +1 −3
  12. apps/local/src/server/mcp-oauth.test.ts +458 −0
  13. packages/plugins/mcp/src/sdk/presets.ts +8 −0
  14. packages/plugins/openapi/src/sdk/plugin.ts +161 −83
  15. packages/plugins/openapi/src/sdk/store.ts +11 −0
apps/cloud/drizzle/0001_harsh_meltdown.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE "openapi_source" ADD COLUMN "source_url" text;
apps/cloud/drizzle/0002_fat_white_tiger.sqlrenamed from apps/cloud/drizzle/0001_funny_betty_brant.sql+0−0

File renamed without changes.

apps/cloud/drizzle/meta/0001_snapshot.json+8−2
@@ -1,5 +1,5 @@
11 {
2− "id": "1c9eefdc-19c2-40cf-b2a7-ae3243a4d731",
2+ "id": "12ebb69b-fb44-4e61-80b9-a48b5471ef5a",
33 "prevId": "93e9f8ef-fe9a-4a8c-b326-329464d876ca",
44 "version": "7",
55 "dialect": "postgresql",
@@ -526,7 +526,7 @@ · 520 unmodified lines
526526 },
527527 "expires_at": {
528528 "name": "expires_at",
529− "type": "bigint",
529+ "type": "integer",
530530 "primaryKey": false,
531531 "notNull": true
532532 },
@@ -801,6 +801,12 @@ · 268 unmodified lines
801801 "primaryKey": false,
802802 "notNull": true
803803 },
804+ "source_url": {
805+ "name": "source_url",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
804810 "base_url": {
805811 "name": "base_url",
806812 "type": "text",
apps/cloud/drizzle/meta/0002_snapshot.json+1276−0
@@ -0,0 +1,1276 @@
1+{
2+ "id": "f029f809-1bf3-46e0-a31b-82df7c9c7171",
3+ "prevId": "12ebb69b-fb44-4e61-80b9-a48b5471ef5a",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": [
64+ "account_id"
65+ ],
66+ "columnsTo": [
67+ "id"
68+ ],
69+ "onDelete": "cascade",
70+ "onUpdate": "no action"
71+ },
72+ "memberships_organization_id_organizations_id_fk": {
73+ "name": "memberships_organization_id_organizations_id_fk",
74+ "tableFrom": "memberships",
75+ "tableTo": "organizations",
76+ "columnsFrom": [
77+ "organization_id"
78+ ],
79+ "columnsTo": [
80+ "id"
81+ ],
82+ "onDelete": "cascade",
83+ "onUpdate": "no action"
84+ }
85+ },
86+ "compositePrimaryKeys": {
87+ "memberships_account_id_organization_id_pk": {
88+ "name": "memberships_account_id_organization_id_pk",
89+ "columns": [
90+ "account_id",
91+ "organization_id"
92+ ]
93+ }
94+ },
95+ "uniqueConstraints": {},
96+ "policies": {},
97+ "checkConstraints": {},
98+ "isRLSEnabled": false
99+ },
100+ "public.organizations": {
101+ "name": "organizations",
102+ "schema": "",
103+ "columns": {
104+ "id": {
105+ "name": "id",
106+ "type": "text",
107+ "primaryKey": true,
108+ "notNull": true
109+ },
110+ "name": {
111+ "name": "name",
112+ "type": "text",
113+ "primaryKey": false,
114+ "notNull": true
115+ },
116+ "created_at": {
117+ "name": "created_at",
118+ "type": "timestamp with time zone",
119+ "primaryKey": false,
120+ "notNull": true,
121+ "default": "now()"
122+ }
123+ },
124+ "indexes": {},
125+ "foreignKeys": {},
126+ "compositePrimaryKeys": {},
127+ "uniqueConstraints": {},
128+ "policies": {},
129+ "checkConstraints": {},
130+ "isRLSEnabled": false
131+ },
132+ "public.blob": {
133+ "name": "blob",
134+ "schema": "",
135+ "columns": {
136+ "namespace": {
137+ "name": "namespace",
138+ "type": "text",
139+ "primaryKey": false,
140+ "notNull": true
141+ },
142+ "key": {
143+ "name": "key",
144+ "type": "text",
145+ "primaryKey": false,
146+ "notNull": true
147+ },
148+ "value": {
149+ "name": "value",
150+ "type": "text",
151+ "primaryKey": false,
152+ "notNull": true
153+ }
154+ },
155+ "indexes": {},
156+ "foreignKeys": {},
157+ "compositePrimaryKeys": {
158+ "blob_namespace_key_pk": {
159+ "name": "blob_namespace_key_pk",
160+ "columns": [
161+ "namespace",
162+ "key"
163+ ]
164+ }
165+ },
166+ "uniqueConstraints": {},
167+ "policies": {},
168+ "checkConstraints": {},
169+ "isRLSEnabled": false
170+ },
171+ "public.definition": {
172+ "name": "definition",
173+ "schema": "",
174+ "columns": {
175+ "id": {
176+ "name": "id",
177+ "type": "text",
178+ "primaryKey": false,
179+ "notNull": true
180+ },
181+ "scope_id": {
182+ "name": "scope_id",
183+ "type": "text",
184+ "primaryKey": false,
185+ "notNull": true
186+ },
187+ "source_id": {
188+ "name": "source_id",
189+ "type": "text",
190+ "primaryKey": false,
191+ "notNull": true
192+ },
193+ "plugin_id": {
194+ "name": "plugin_id",
195+ "type": "text",
196+ "primaryKey": false,
197+ "notNull": true
198+ },
199+ "name": {
200+ "name": "name",
201+ "type": "text",
202+ "primaryKey": false,
203+ "notNull": true
204+ },
205+ "schema": {
206+ "name": "schema",
207+ "type": "jsonb",
208+ "primaryKey": false,
209+ "notNull": true
210+ },
211+ "created_at": {
212+ "name": "created_at",
213+ "type": "timestamp",
214+ "primaryKey": false,
215+ "notNull": true
216+ }
217+ },
218+ "indexes": {
219+ "definition_scope_id_idx": {
220+ "name": "definition_scope_id_idx",
221+ "columns": [
222+ {
223+ "expression": "scope_id",
224+ "isExpression": false,
225+ "asc": true,
226+ "nulls": "last"
227+ }
228+ ],
229+ "isUnique": false,
230+ "concurrently": false,
231+ "method": "btree",
232+ "with": {}
233+ },
234+ "definition_source_id_idx": {
235+ "name": "definition_source_id_idx",
236+ "columns": [
237+ {
238+ "expression": "source_id",
239+ "isExpression": false,
240+ "asc": true,
241+ "nulls": "last"
242+ }
243+ ],
244+ "isUnique": false,
245+ "concurrently": false,
246+ "method": "btree",
247+ "with": {}
248+ },
249+ "definition_plugin_id_idx": {
250+ "name": "definition_plugin_id_idx",
251+ "columns": [
252+ {
253+ "expression": "plugin_id",
254+ "isExpression": false,
255+ "asc": true,
256+ "nulls": "last"
257+ }
258+ ],
259+ "isUnique": false,
260+ "concurrently": false,
261+ "method": "btree",
262+ "with": {}
263+ }
264+ },
265+ "foreignKeys": {},
266+ "compositePrimaryKeys": {
267+ "definition_scope_id_id_pk": {
268+ "name": "definition_scope_id_id_pk",
269+ "columns": [
270+ "scope_id",
271+ "id"
272+ ]
273+ }
274+ },
275+ "uniqueConstraints": {},
276+ "policies": {},
277+ "checkConstraints": {},
278+ "isRLSEnabled": false
279+ },
280+ "public.graphql_operation": {
281+ "name": "graphql_operation",
282+ "schema": "",
283+ "columns": {
284+ "id": {
285+ "name": "id",
286+ "type": "text",
287+ "primaryKey": false,
288+ "notNull": true
289+ },
290+ "scope_id": {
291+ "name": "scope_id",
292+ "type": "text",
293+ "primaryKey": false,
294+ "notNull": true
295+ },
296+ "source_id": {
297+ "name": "source_id",
298+ "type": "text",
299+ "primaryKey": false,
300+ "notNull": true
301+ },
302+ "binding": {
303+ "name": "binding",
304+ "type": "jsonb",
305+ "primaryKey": false,
306+ "notNull": true
307+ }
308+ },
309+ "indexes": {
310+ "graphql_operation_scope_id_idx": {
311+ "name": "graphql_operation_scope_id_idx",
312+ "columns": [
313+ {
314+ "expression": "scope_id",
315+ "isExpression": false,
316+ "asc": true,
317+ "nulls": "last"
318+ }
319+ ],
320+ "isUnique": false,
321+ "concurrently": false,
322+ "method": "btree",
323+ "with": {}
324+ },
325+ "graphql_operation_source_id_idx": {
326+ "name": "graphql_operation_source_id_idx",
327+ "columns": [
328+ {
329+ "expression": "source_id",
330+ "isExpression": false,
331+ "asc": true,
332+ "nulls": "last"
333+ }
334+ ],
335+ "isUnique": false,
336+ "concurrently": false,
337+ "method": "btree",
338+ "with": {}
339+ }
340+ },
341+ "foreignKeys": {},
342+ "compositePrimaryKeys": {
343+ "graphql_operation_scope_id_id_pk": {
344+ "name": "graphql_operation_scope_id_id_pk",
345+ "columns": [
346+ "scope_id",
347+ "id"
348+ ]
349+ }
350+ },
351+ "uniqueConstraints": {},
352+ "policies": {},
353+ "checkConstraints": {},
354+ "isRLSEnabled": false
355+ },
356+ "public.graphql_source": {
357+ "name": "graphql_source",
358+ "schema": "",
359+ "columns": {
360+ "id": {
361+ "name": "id",
362+ "type": "text",
363+ "primaryKey": false,
364+ "notNull": true
365+ },
366+ "scope_id": {
367+ "name": "scope_id",
368+ "type": "text",
369+ "primaryKey": false,
370+ "notNull": true
371+ },
372+ "name": {
373+ "name": "name",
374+ "type": "text",
375+ "primaryKey": false,
376+ "notNull": true
377+ },
378+ "endpoint": {
379+ "name": "endpoint",
380+ "type": "text",
381+ "primaryKey": false,
382+ "notNull": true
383+ },
384+ "headers": {
385+ "name": "headers",
386+ "type": "jsonb",
387+ "primaryKey": false,
388+ "notNull": false
389+ }
390+ },
391+ "indexes": {
392+ "graphql_source_scope_id_idx": {
393+ "name": "graphql_source_scope_id_idx",
394+ "columns": [
395+ {
396+ "expression": "scope_id",
397+ "isExpression": false,
398+ "asc": true,
399+ "nulls": "last"
400+ }
401+ ],
402+ "isUnique": false,
403+ "concurrently": false,
404+ "method": "btree",
405+ "with": {}
406+ }
407+ },
408+ "foreignKeys": {},
409+ "compositePrimaryKeys": {
410+ "graphql_source_scope_id_id_pk": {
411+ "name": "graphql_source_scope_id_id_pk",
412+ "columns": [
413+ "scope_id",
414+ "id"
415+ ]
416+ }
417+ },
418+ "uniqueConstraints": {},
419+ "policies": {},
420+ "checkConstraints": {},
421+ "isRLSEnabled": false
422+ },
423+ "public.mcp_binding": {
424+ "name": "mcp_binding",
425+ "schema": "",
426+ "columns": {
427+ "id": {
428+ "name": "id",
429+ "type": "text",
430+ "primaryKey": false,
431+ "notNull": true
432+ },
433+ "scope_id": {
434+ "name": "scope_id",
435+ "type": "text",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "source_id": {
440+ "name": "source_id",
441+ "type": "text",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "binding": {
446+ "name": "binding",
447+ "type": "jsonb",
448+ "primaryKey": false,
449+ "notNull": true
450+ },
451+ "created_at": {
452+ "name": "created_at",
453+ "type": "timestamp",
454+ "primaryKey": false,
455+ "notNull": true
456+ }
457+ },
458+ "indexes": {
459+ "mcp_binding_scope_id_idx": {
460+ "name": "mcp_binding_scope_id_idx",
461+ "columns": [
462+ {
463+ "expression": "scope_id",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ }
468+ ],
469+ "isUnique": false,
470+ "concurrently": false,
471+ "method": "btree",
472+ "with": {}
473+ },
474+ "mcp_binding_source_id_idx": {
475+ "name": "mcp_binding_source_id_idx",
476+ "columns": [
477+ {
478+ "expression": "source_id",
479+ "isExpression": false,
480+ "asc": true,
481+ "nulls": "last"
482+ }
483+ ],
484+ "isUnique": false,
485+ "concurrently": false,
486+ "method": "btree",
487+ "with": {}
488+ }
489+ },
490+ "foreignKeys": {},
491+ "compositePrimaryKeys": {
492+ "mcp_binding_scope_id_id_pk": {
493+ "name": "mcp_binding_scope_id_id_pk",
494+ "columns": [
495+ "scope_id",
496+ "id"
497+ ]
498+ }
499+ },
500+ "uniqueConstraints": {},
501+ "policies": {},
502+ "checkConstraints": {},
503+ "isRLSEnabled": false
504+ },
505+ "public.mcp_oauth_session": {
506+ "name": "mcp_oauth_session",
507+ "schema": "",
508+ "columns": {
509+ "id": {
510+ "name": "id",
511+ "type": "text",
512+ "primaryKey": false,
513+ "notNull": true
514+ },
515+ "scope_id": {
516+ "name": "scope_id",
517+ "type": "text",
518+ "primaryKey": false,
519+ "notNull": true
520+ },
521+ "session": {
522+ "name": "session",
523+ "type": "jsonb",
524+ "primaryKey": false,
525+ "notNull": true
526+ },
527+ "expires_at": {
528+ "name": "expires_at",
529+ "type": "bigint",
530+ "primaryKey": false,
531+ "notNull": true
532+ },
533+ "created_at": {
534+ "name": "created_at",
535+ "type": "timestamp",
536+ "primaryKey": false,
537+ "notNull": true
538+ }
539+ },
540+ "indexes": {
541+ "mcp_oauth_session_scope_id_idx": {
542+ "name": "mcp_oauth_session_scope_id_idx",
543+ "columns": [
544+ {
545+ "expression": "scope_id",
546+ "isExpression": false,
547+ "asc": true,
548+ "nulls": "last"
549+ }
550+ ],
551+ "isUnique": false,
552+ "concurrently": false,
553+ "method": "btree",
554+ "with": {}
555+ }
556+ },
557+ "foreignKeys": {},
558+ "compositePrimaryKeys": {
559+ "mcp_oauth_session_scope_id_id_pk": {
560+ "name": "mcp_oauth_session_scope_id_id_pk",
561+ "columns": [
562+ "scope_id",
563+ "id"
564+ ]
565+ }
566+ },
567+ "uniqueConstraints": {},
568+ "policies": {},
569+ "checkConstraints": {},
570+ "isRLSEnabled": false
571+ },
572+ "public.mcp_source": {
573+ "name": "mcp_source",
574+ "schema": "",
575+ "columns": {
576+ "id": {
577+ "name": "id",
578+ "type": "text",
579+ "primaryKey": false,
580+ "notNull": true
581+ },
582+ "scope_id": {
583+ "name": "scope_id",
584+ "type": "text",
585+ "primaryKey": false,
586+ "notNull": true
587+ },
588+ "name": {
589+ "name": "name",
590+ "type": "text",
591+ "primaryKey": false,
592+ "notNull": true
593+ },
594+ "config": {
595+ "name": "config",
596+ "type": "jsonb",
597+ "primaryKey": false,
598+ "notNull": true
599+ },
600+ "created_at": {
601+ "name": "created_at",
602+ "type": "timestamp",
603+ "primaryKey": false,
604+ "notNull": true
605+ }
606+ },
607+ "indexes": {
608+ "mcp_source_scope_id_idx": {
609+ "name": "mcp_source_scope_id_idx",
610+ "columns": [
611+ {
612+ "expression": "scope_id",
613+ "isExpression": false,
614+ "asc": true,
615+ "nulls": "last"
616+ }
617+ ],
618+ "isUnique": false,
619+ "concurrently": false,
620+ "method": "btree",
621+ "with": {}
622+ }
623+ },
624+ "foreignKeys": {},
625+ "compositePrimaryKeys": {
626+ "mcp_source_scope_id_id_pk": {
627+ "name": "mcp_source_scope_id_id_pk",
628+ "columns": [
629+ "scope_id",
630+ "id"
631+ ]
632+ }
633+ },
634+ "uniqueConstraints": {},
635+ "policies": {},
636+ "checkConstraints": {},
637+ "isRLSEnabled": false
638+ },
639+ "public.openapi_oauth_session": {
640+ "name": "openapi_oauth_session",
641+ "schema": "",
642+ "columns": {
643+ "id": {
644+ "name": "id",
645+ "type": "text",
646+ "primaryKey": false,
647+ "notNull": true
648+ },
649+ "scope_id": {
650+ "name": "scope_id",
651+ "type": "text",
652+ "primaryKey": false,
653+ "notNull": true
654+ },
655+ "session": {
656+ "name": "session",
657+ "type": "jsonb",
658+ "primaryKey": false,
659+ "notNull": true
660+ },
661+ "created_at": {
662+ "name": "created_at",
663+ "type": "timestamp",
664+ "primaryKey": false,
665+ "notNull": true
666+ }
667+ },
668+ "indexes": {
669+ "openapi_oauth_session_scope_id_idx": {
670+ "name": "openapi_oauth_session_scope_id_idx",
671+ "columns": [
672+ {
673+ "expression": "scope_id",
674+ "isExpression": false,
675+ "asc": true,
676+ "nulls": "last"
677+ }
678+ ],
679+ "isUnique": false,
680+ "concurrently": false,
681+ "method": "btree",
682+ "with": {}
683+ }
684+ },
685+ "foreignKeys": {},
686+ "compositePrimaryKeys": {
687+ "openapi_oauth_session_scope_id_id_pk": {
688+ "name": "openapi_oauth_session_scope_id_id_pk",
689+ "columns": [
690+ "scope_id",
691+ "id"
692+ ]
693+ }
694+ },
695+ "uniqueConstraints": {},
696+ "policies": {},
697+ "checkConstraints": {},
698+ "isRLSEnabled": false
699+ },
700+ "public.openapi_operation": {
701+ "name": "openapi_operation",
702+ "schema": "",
703+ "columns": {
704+ "id": {
705+ "name": "id",
706+ "type": "text",
707+ "primaryKey": false,
708+ "notNull": true
709+ },
710+ "scope_id": {
711+ "name": "scope_id",
712+ "type": "text",
713+ "primaryKey": false,
714+ "notNull": true
715+ },
716+ "source_id": {
717+ "name": "source_id",
718+ "type": "text",
719+ "primaryKey": false,
720+ "notNull": true
721+ },
722+ "binding": {
723+ "name": "binding",
724+ "type": "jsonb",
725+ "primaryKey": false,
726+ "notNull": true
727+ }
728+ },
729+ "indexes": {
730+ "openapi_operation_scope_id_idx": {
731+ "name": "openapi_operation_scope_id_idx",
732+ "columns": [
733+ {
734+ "expression": "scope_id",
735+ "isExpression": false,
736+ "asc": true,
737+ "nulls": "last"
738+ }
739+ ],
740+ "isUnique": false,
741+ "concurrently": false,
742+ "method": "btree",
743+ "with": {}
744+ },
745+ "openapi_operation_source_id_idx": {
746+ "name": "openapi_operation_source_id_idx",
747+ "columns": [
748+ {
749+ "expression": "source_id",
750+ "isExpression": false,
751+ "asc": true,
752+ "nulls": "last"
753+ }
754+ ],
755+ "isUnique": false,
756+ "concurrently": false,
757+ "method": "btree",
758+ "with": {}
759+ }
760+ },
761+ "foreignKeys": {},
762+ "compositePrimaryKeys": {
763+ "openapi_operation_scope_id_id_pk": {
764+ "name": "openapi_operation_scope_id_id_pk",
765+ "columns": [
766+ "scope_id",
767+ "id"
768+ ]
769+ }
770+ },
771+ "uniqueConstraints": {},
772+ "policies": {},
773+ "checkConstraints": {},
774+ "isRLSEnabled": false
775+ },
776+ "public.openapi_source": {
777+ "name": "openapi_source",
778+ "schema": "",
779+ "columns": {
780+ "id": {
781+ "name": "id",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": true
785+ },
786+ "scope_id": {
787+ "name": "scope_id",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": true
791+ },
792+ "name": {
793+ "name": "name",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": true
797+ },
798+ "spec": {
799+ "name": "spec",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": true
803+ },
804+ "source_url": {
805+ "name": "source_url",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
810+ "base_url": {
811+ "name": "base_url",
812+ "type": "text",
813+ "primaryKey": false,
814+ "notNull": false
815+ },
816+ "headers": {
817+ "name": "headers",
818+ "type": "jsonb",
819+ "primaryKey": false,
820+ "notNull": false
821+ },
822+ "oauth2": {
823+ "name": "oauth2",
824+ "type": "jsonb",
825+ "primaryKey": false,
826+ "notNull": false
827+ },
828+ "invocation_config": {
829+ "name": "invocation_config",
830+ "type": "jsonb",
831+ "primaryKey": false,
832+ "notNull": true
833+ }
834+ },
835+ "indexes": {
836+ "openapi_source_scope_id_idx": {
837+ "name": "openapi_source_scope_id_idx",
838+ "columns": [
839+ {
840+ "expression": "scope_id",
841+ "isExpression": false,
842+ "asc": true,
843+ "nulls": "last"
844+ }
845+ ],
846+ "isUnique": false,
847+ "concurrently": false,
848+ "method": "btree",
849+ "with": {}
850+ }
851+ },
852+ "foreignKeys": {},
853+ "compositePrimaryKeys": {
854+ "openapi_source_scope_id_id_pk": {
855+ "name": "openapi_source_scope_id_id_pk",
856+ "columns": [
857+ "scope_id",
858+ "id"
859+ ]
860+ }
861+ },
862+ "uniqueConstraints": {},
863+ "policies": {},
864+ "checkConstraints": {},
865+ "isRLSEnabled": false
866+ },
867+ "public.secret": {
868+ "name": "secret",
869+ "schema": "",
870+ "columns": {
871+ "id": {
872+ "name": "id",
873+ "type": "text",
874+ "primaryKey": false,
875+ "notNull": true
876+ },
877+ "scope_id": {
878+ "name": "scope_id",
879+ "type": "text",
880+ "primaryKey": false,
881+ "notNull": true
882+ },
883+ "name": {
884+ "name": "name",
885+ "type": "text",
886+ "primaryKey": false,
887+ "notNull": true
888+ },
889+ "provider": {
890+ "name": "provider",
891+ "type": "text",
892+ "primaryKey": false,
893+ "notNull": true
894+ },
895+ "created_at": {
896+ "name": "created_at",
897+ "type": "timestamp",
898+ "primaryKey": false,
899+ "notNull": true
900+ }
901+ },
902+ "indexes": {
903+ "secret_scope_id_idx": {
904+ "name": "secret_scope_id_idx",
905+ "columns": [
906+ {
907+ "expression": "scope_id",
908+ "isExpression": false,
909+ "asc": true,
910+ "nulls": "last"
911+ }
912+ ],
913+ "isUnique": false,
914+ "concurrently": false,
915+ "method": "btree",
916+ "with": {}
917+ },
918+ "secret_provider_idx": {
919+ "name": "secret_provider_idx",
920+ "columns": [
921+ {
922+ "expression": "provider",
923+ "isExpression": false,
924+ "asc": true,
925+ "nulls": "last"
926+ }
927+ ],
928+ "isUnique": false,
929+ "concurrently": false,
930+ "method": "btree",
931+ "with": {}
932+ }
933+ },
934+ "foreignKeys": {},
935+ "compositePrimaryKeys": {
936+ "secret_scope_id_id_pk": {
937+ "name": "secret_scope_id_id_pk",
938+ "columns": [
939+ "scope_id",
940+ "id"
941+ ]
942+ }
943+ },
944+ "uniqueConstraints": {},
945+ "policies": {},
946+ "checkConstraints": {},
947+ "isRLSEnabled": false
948+ },
949+ "public.source": {
950+ "name": "source",
951+ "schema": "",
952+ "columns": {
953+ "id": {
954+ "name": "id",
955+ "type": "text",
956+ "primaryKey": false,
957+ "notNull": true
958+ },
959+ "scope_id": {
960+ "name": "scope_id",
961+ "type": "text",
962+ "primaryKey": false,
963+ "notNull": true
964+ },
965+ "plugin_id": {
966+ "name": "plugin_id",
967+ "type": "text",
968+ "primaryKey": false,
969+ "notNull": true
970+ },
971+ "kind": {
972+ "name": "kind",
973+ "type": "text",
974+ "primaryKey": false,
975+ "notNull": true
976+ },
977+ "name": {
978+ "name": "name",
979+ "type": "text",
980+ "primaryKey": false,
981+ "notNull": true
982+ },
983+ "url": {
984+ "name": "url",
985+ "type": "text",
986+ "primaryKey": false,
987+ "notNull": false
988+ },
989+ "can_remove": {
990+ "name": "can_remove",
991+ "type": "boolean",
992+ "primaryKey": false,
993+ "notNull": true,
994+ "default": true
995+ },
996+ "can_refresh": {
997+ "name": "can_refresh",
998+ "type": "boolean",
999+ "primaryKey": false,
1000+ "notNull": true,
1001+ "default": false
1002+ },
1003+ "can_edit": {
1004+ "name": "can_edit",
1005+ "type": "boolean",
1006+ "primaryKey": false,
1007+ "notNull": true,
1008+ "default": false
1009+ },
1010+ "created_at": {
1011+ "name": "created_at",
1012+ "type": "timestamp",
1013+ "primaryKey": false,
1014+ "notNull": true
1015+ },
1016+ "updated_at": {
1017+ "name": "updated_at",
1018+ "type": "timestamp",
1019+ "primaryKey": false,
1020+ "notNull": true
1021+ }
1022+ },
1023+ "indexes": {
1024+ "source_scope_id_idx": {
1025+ "name": "source_scope_id_idx",
1026+ "columns": [
1027+ {
1028+ "expression": "scope_id",
1029+ "isExpression": false,
1030+ "asc": true,
1031+ "nulls": "last"
1032+ }
1033+ ],
1034+ "isUnique": false,
1035+ "concurrently": false,
1036+ "method": "btree",
1037+ "with": {}
1038+ },
1039+ "source_plugin_id_idx": {
1040+ "name": "source_plugin_id_idx",
1041+ "columns": [
1042+ {
1043+ "expression": "plugin_id",
1044+ "isExpression": false,
1045+ "asc": true,
1046+ "nulls": "last"
1047+ }
1048+ ],
1049+ "isUnique": false,
1050+ "concurrently": false,
1051+ "method": "btree",
1052+ "with": {}
1053+ }
1054+ },
1055+ "foreignKeys": {},
1056+ "compositePrimaryKeys": {
1057+ "source_scope_id_id_pk": {
1058+ "name": "source_scope_id_id_pk",
1059+ "columns": [
1060+ "scope_id",
1061+ "id"
1062+ ]
1063+ }
1064+ },
1065+ "uniqueConstraints": {},
1066+ "policies": {},
1067+ "checkConstraints": {},
1068+ "isRLSEnabled": false
1069+ },
1070+ "public.tool": {
1071+ "name": "tool",
1072+ "schema": "",
1073+ "columns": {
1074+ "id": {
1075+ "name": "id",
1076+ "type": "text",
1077+ "primaryKey": false,
1078+ "notNull": true
1079+ },
1080+ "scope_id": {
1081+ "name": "scope_id",
1082+ "type": "text",
1083+ "primaryKey": false,
1084+ "notNull": true
1085+ },
1086+ "source_id": {
1087+ "name": "source_id",
1088+ "type": "text",
1089+ "primaryKey": false,
1090+ "notNull": true
1091+ },
1092+ "plugin_id": {
1093+ "name": "plugin_id",
1094+ "type": "text",
1095+ "primaryKey": false,
1096+ "notNull": true
1097+ },
1098+ "name": {
1099+ "name": "name",
1100+ "type": "text",
1101+ "primaryKey": false,
1102+ "notNull": true
1103+ },
1104+ "description": {
1105+ "name": "description",
1106+ "type": "text",
1107+ "primaryKey": false,
1108+ "notNull": true
1109+ },
1110+ "input_schema": {
1111+ "name": "input_schema",
1112+ "type": "jsonb",
1113+ "primaryKey": false,
1114+ "notNull": false
1115+ },
1116+ "output_schema": {
1117+ "name": "output_schema",
1118+ "type": "jsonb",
1119+ "primaryKey": false,
1120+ "notNull": false
1121+ },
1122+ "created_at": {
1123+ "name": "created_at",
1124+ "type": "timestamp",
1125+ "primaryKey": false,
1126+ "notNull": true
1127+ },
1128+ "updated_at": {
1129+ "name": "updated_at",
1130+ "type": "timestamp",
1131+ "primaryKey": false,
1132+ "notNull": true
1133+ }
1134+ },
1135+ "indexes": {
1136+ "tool_scope_id_idx": {
1137+ "name": "tool_scope_id_idx",
1138+ "columns": [
1139+ {
1140+ "expression": "scope_id",
1141+ "isExpression": false,
1142+ "asc": true,
1143+ "nulls": "last"
1144+ }
1145+ ],
1146+ "isUnique": false,
1147+ "concurrently": false,
1148+ "method": "btree",
1149+ "with": {}
1150+ },
1151+ "tool_source_id_idx": {
1152+ "name": "tool_source_id_idx",
1153+ "columns": [
1154+ {
1155+ "expression": "source_id",
1156+ "isExpression": false,
1157+ "asc": true,
1158+ "nulls": "last"
1159+ }
1160+ ],
1161+ "isUnique": false,
1162+ "concurrently": false,
1163+ "method": "btree",
1164+ "with": {}
1165+ },
1166+ "tool_plugin_id_idx": {
1167+ "name": "tool_plugin_id_idx",
1168+ "columns": [
1169+ {
1170+ "expression": "plugin_id",
1171+ "isExpression": false,
1172+ "asc": true,
1173+ "nulls": "last"
1174+ }
1175+ ],
1176+ "isUnique": false,
1177+ "concurrently": false,
1178+ "method": "btree",
1179+ "with": {}
1180+ }
1181+ },
1182+ "foreignKeys": {},
1183+ "compositePrimaryKeys": {
1184+ "tool_scope_id_id_pk": {
1185+ "name": "tool_scope_id_id_pk",
1186+ "columns": [
1187+ "scope_id",
1188+ "id"
1189+ ]
1190+ }
1191+ },
1192+ "uniqueConstraints": {},
1193+ "policies": {},
1194+ "checkConstraints": {},
1195+ "isRLSEnabled": false
1196+ },
1197+ "public.workos_vault_metadata": {
1198+ "name": "workos_vault_metadata",
1199+ "schema": "",
1200+ "columns": {
1201+ "id": {
1202+ "name": "id",
1203+ "type": "text",
1204+ "primaryKey": false,
1205+ "notNull": true
1206+ },
1207+ "scope_id": {
1208+ "name": "scope_id",
1209+ "type": "text",
1210+ "primaryKey": false,
1211+ "notNull": true
1212+ },
1213+ "name": {
1214+ "name": "name",
1215+ "type": "text",
1216+ "primaryKey": false,
1217+ "notNull": true
1218+ },
1219+ "purpose": {
1220+ "name": "purpose",
1221+ "type": "text",
1222+ "primaryKey": false,
1223+ "notNull": false
1224+ },
1225+ "created_at": {
1226+ "name": "created_at",
1227+ "type": "timestamp",
1228+ "primaryKey": false,
1229+ "notNull": true
1230+ }
1231+ },
1232+ "indexes": {
1233+ "workos_vault_metadata_scope_id_idx": {
1234+ "name": "workos_vault_metadata_scope_id_idx",
1235+ "columns": [
1236+ {
1237+ "expression": "scope_id",
1238+ "isExpression": false,
1239+ "asc": true,
1240+ "nulls": "last"
1241+ }
1242+ ],
1243+ "isUnique": false,
1244+ "concurrently": false,
1245+ "method": "btree",
1246+ "with": {}
1247+ }
1248+ },
1249+ "foreignKeys": {},
1250+ "compositePrimaryKeys": {
1251+ "workos_vault_metadata_scope_id_id_pk": {
1252+ "name": "workos_vault_metadata_scope_id_id_pk",
1253+ "columns": [
1254+ "scope_id",
1255+ "id"
1256+ ]
1257+ }
1258+ },
1259+ "uniqueConstraints": {},
1260+ "policies": {},
1261+ "checkConstraints": {},
1262+ "isRLSEnabled": false
1263+ }
1264+ },
1265+ "enums": {},
1266+ "schemas": {},
1267+ "sequences": {},
1268+ "roles": {},
1269+ "policies": {},
1270+ "views": {},
1271+ "_meta": {
1272+ "columns": {},
1273+ "schemas": {},
1274+ "tables": {}
1275+ }
1276+}
apps/cloud/drizzle/meta/_journal.json+9−2
@@ -12,8 +12,15 @@ · 11 unmodified lines
1212 {
1313 "idx": 1,
1414 "version": "7",
15− "when": 1776706145641,
16− "tag": "0001_funny_betty_brant",
15+ "when": 1776678968180,
16+ "tag": "0001_harsh_meltdown",
17+ "breakpoints": true
18+ },
19+ {
20+ "idx": 2,
21+ "version": "7",
22+ "when": 1776709954401,
23+ "tag": "0002_fat_white_tiger",
1724 "breakpoints": true
1825 }
1926 ]
apps/cloud/src/services/executor-schema.ts+2−0
@@ -68,6 +68,7 @@ export const openapi_source = pgTable("openapi_source", { · 67 unmodified lines
6868 scope_id: text('scope_id').notNull(),
6969 name: text('name').notNull(),
7070 spec: text('spec').notNull(),
71+ source_url: text('source_url'),
7172 base_url: text('base_url'),
7273 headers: jsonb('headers'),
7374 oauth2: jsonb('oauth2'),
@@ -176,3 +177,4 @@ export const blob = pgTable("blob", { · 102 unmodified lines
176177 primaryKey({ columns: [table.namespace, table.key] }),
177178 ]);
178179
180+
apps/cloud/src/services/sources-refresh.node.test.ts+177−0
@@ -0,0 +1,177 @@
1+// Refresh endpoint — covers `sources.refresh(id)` for an OpenAPI
2+// source added from a URL. Stands up a local HTTP server that serves
3+// one of two spec versions (swappable mid-test) so we can verify the
4+// refresh path re-fetches from the stored origin and replaces the
5+// operation set. Raw-text sources assert the no-op branch.
6+
7+import { describe, expect, it } from "@effect/vitest";
8+import { Effect } from "effect";
9+import http from "node:http";
10+import { AddressInfo } from "node:net";
11+
12+import { ScopeId } from "@executor/sdk";
13+
14+import { asOrg } from "./__test-harness__/api-harness";
15+
16+const specV1 = JSON.stringify({
17+ openapi: "3.0.0",
18+ info: { title: "Refresh Fixture", version: "1.0.0" },
19+ paths: {
20+ "/ping": {
21+ get: {
22+ operationId: "ping",
23+ summary: "ping",
24+ responses: { "200": { description: "ok" } },
25+ },
26+ },
27+ },
28+});
29+
30+const specV2 = JSON.stringify({
31+ openapi: "3.0.0",
32+ info: { title: "Refresh Fixture", version: "2.0.0" },
33+ paths: {
34+ "/ping": {
35+ get: {
36+ operationId: "ping",
37+ summary: "ping",
38+ responses: { "200": { description: "ok" } },
39+ },
40+ },
41+ "/pong": {
42+ get: {
43+ operationId: "pong",
44+ summary: "pong",
45+ responses: { "200": { description: "ok" } },
46+ },
47+ },
48+ },
49+});
50+
51+// Mutable ref: tests flip `current` between v1 and v2 around the
52+// refresh call. Using a single server keeps the URL stable across
53+// both addSpec and refresh — the plugin persists the original URL,
54+// so the second fetch goes back to the same endpoint.
55+const serveMutableSpec = () => {
56+ const state = { current: specV1, requests: 0 };
57+ const server = http.createServer((req, res) => {
58+ state.requests++;
59+ res.writeHead(200, { "content-type": "application/json" });
60+ res.end(state.current);
61+ });
62+ return new Promise<{
63+ baseUrl: string;
64+ setSpec: (s: string) => void;
65+ requestCount: () => number;
66+ close: () => Promise<void>;
67+ }>((resolve) => {
68+ server.listen(0, "127.0.0.1", () => {
69+ const { port } = server.address() as AddressInfo;
70+ resolve({
71+ baseUrl: `http://127.0.0.1:${port}`,
72+ setSpec: (s) => {
73+ state.current = s;
74+ },
75+ requestCount: () => state.requests,
76+ close: () =>
77+ new Promise((r) => {
78+ server.close(() => r());
79+ }),
80+ });
81+ });
82+ });
83+};
84+
85+describe("sources.refresh (HTTP)", () => {
86+ it.effect("addSpec from URL → canRefresh:true; refresh re-fetches and updates tools", () =>
87+ Effect.gen(function* () {
88+ const server = yield* Effect.promise(() => serveMutableSpec());
89+ try {
90+ const org = `org_${crypto.randomUUID()}`;
91+ const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;
92+
93+ yield* asOrg(org, (client) =>
94+ client.openapi.addSpec({
95+ path: { scopeId: ScopeId.make(org) },
96+ payload: { spec: `${server.baseUrl}/spec.json`, namespace },
97+ }),
98+ );
99+
100+ const before = yield* asOrg(org, (client) =>
101+ client.sources.list({ path: { scopeId: ScopeId.make(org) } }),
102+ );
103+ const beforeSource = before.find((s) => s.id === namespace);
104+ expect(beforeSource?.canRefresh).toBe(true);
105+
106+ const fetchedBefore = yield* asOrg(org, (client) =>
107+ client.openapi.getSource({
108+ path: { scopeId: ScopeId.make(org), namespace },
109+ }),
110+ );
111+ expect(fetchedBefore?.config.sourceUrl).toBe(`${server.baseUrl}/spec.json`);
112+
113+ const beforeTools = yield* asOrg(org, (client) =>
114+ client.sources.tools({
115+ path: { scopeId: ScopeId.make(org), sourceId: namespace },
116+ }),
117+ );
118+ expect(beforeTools.length).toBe(1);
119+ expect(beforeTools.some((t) => t.name.startsWith("ping"))).toBe(true);
120+ expect(beforeTools.some((t) => t.name.startsWith("pong"))).toBe(false);
121+
122+ // Flip the remote to v2 (adds `pong`) and trigger refresh.
123+ server.setSpec(specV2);
124+ const requestsBefore = server.requestCount();
125+
126+ const refreshResult = yield* asOrg(org, (client) =>
127+ client.sources.refresh({
128+ path: { scopeId: ScopeId.make(org), sourceId: namespace },
129+ }),
130+ );
131+ expect(refreshResult.refreshed).toBe(true);
132+ expect(server.requestCount()).toBeGreaterThan(requestsBefore);
133+
134+ const afterTools = yield* asOrg(org, (client) =>
135+ client.sources.tools({
136+ path: { scopeId: ScopeId.make(org), sourceId: namespace },
137+ }),
138+ );
139+ expect(afterTools.length).toBe(2);
140+ expect(afterTools.some((t) => t.name.startsWith("ping"))).toBe(true);
141+ expect(afterTools.some((t) => t.name.startsWith("pong"))).toBe(true);
142+ } finally {
143+ yield* Effect.promise(() => server.close());
144+ }
145+ }),
146+ );
147+
148+ it.effect("addSpec from raw text → canRefresh:false; refresh is a no-op", () =>
149+ Effect.gen(function* () {
150+ const org = `org_${crypto.randomUUID()}`;
151+ const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;
152+
153+ yield* asOrg(org, (client) =>
154+ client.openapi.addSpec({
155+ path: { scopeId: ScopeId.make(org) },
156+ payload: { spec: specV1, namespace },
157+ }),
158+ );
159+
160+ const sources = yield* asOrg(org, (client) =>
161+ client.sources.list({ path: { scopeId: ScopeId.make(org) } }),
162+ );
163+ const row = sources.find((s) => s.id === namespace);
164+ expect(row?.canRefresh).toBe(false);
165+
166+ // Raw-text sources reach the plugin with no stored URL and
167+ // silently no-op — UI gates the action on canRefresh, but the
168+ // server should not 500 if a caller slips through.
169+ const result = yield* asOrg(org, (client) =>
170+ client.sources.refresh({
171+ path: { scopeId: ScopeId.make(org), sourceId: namespace },
172+ }),
173+ );
174+ expect(result.refreshed).toBe(true);
175+ }),
176+ );
177+});
apps/local/drizzle/0001_sour_catseye.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE `openapi_source` ADD `source_url` text;
apps/local/drizzle/meta/0001_snapshot.json+1143−0
@@ -0,0 +1,1143 @@
1+{
2+ "version": "6",
3+ "dialect": "sqlite",
4+ "id": "acff0d94-170c-40bb-9271-dc3a429ba0ce",
5+ "prevId": "61fe33e0-7218-468e-8568-9a3f19e821ee",
6+ "tables": {
7+ "blob": {
8+ "name": "blob",
9+ "columns": {
10+ "namespace": {
11+ "name": "namespace",
12+ "type": "text",
13+ "primaryKey": false,
14+ "notNull": true,
15+ "autoincrement": false
16+ },
17+ "key": {
18+ "name": "key",
19+ "type": "text",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "autoincrement": false
23+ },
24+ "value": {
25+ "name": "value",
26+ "type": "text",
27+ "primaryKey": false,
28+ "notNull": true,
29+ "autoincrement": false
30+ }
31+ },
32+ "indexes": {},
33+ "foreignKeys": {},
34+ "compositePrimaryKeys": {
35+ "blob_namespace_key_pk": {
36+ "columns": [
37+ "namespace",
38+ "key"
39+ ],
40+ "name": "blob_namespace_key_pk"
41+ }
42+ },
43+ "uniqueConstraints": {},
44+ "checkConstraints": {}
45+ },
46+ "definition": {
47+ "name": "definition",
48+ "columns": {
49+ "id": {
50+ "name": "id",
51+ "type": "text",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "autoincrement": false
55+ },
56+ "scope_id": {
57+ "name": "scope_id",
58+ "type": "text",
59+ "primaryKey": false,
60+ "notNull": true,
61+ "autoincrement": false
62+ },
63+ "source_id": {
64+ "name": "source_id",
65+ "type": "text",
66+ "primaryKey": false,
67+ "notNull": true,
68+ "autoincrement": false
69+ },
70+ "plugin_id": {
71+ "name": "plugin_id",
72+ "type": "text",
73+ "primaryKey": false,
74+ "notNull": true,
75+ "autoincrement": false
76+ },
77+ "name": {
78+ "name": "name",
79+ "type": "text",
80+ "primaryKey": false,
81+ "notNull": true,
82+ "autoincrement": false
83+ },
84+ "schema": {
85+ "name": "schema",
86+ "type": "text",
87+ "primaryKey": false,
88+ "notNull": true,
89+ "autoincrement": false
90+ },
91+ "created_at": {
92+ "name": "created_at",
93+ "type": "integer",
94+ "primaryKey": false,
95+ "notNull": true,
96+ "autoincrement": false
97+ }
98+ },
99+ "indexes": {
100+ "definition_scope_id_idx": {
101+ "name": "definition_scope_id_idx",
102+ "columns": [
103+ "scope_id"
104+ ],
105+ "isUnique": false
106+ },
107+ "definition_source_id_idx": {
108+ "name": "definition_source_id_idx",
109+ "columns": [
110+ "source_id"
111+ ],
112+ "isUnique": false
113+ },
114+ "definition_plugin_id_idx": {
115+ "name": "definition_plugin_id_idx",
116+ "columns": [
117+ "plugin_id"
118+ ],
119+ "isUnique": false
120+ }
121+ },
122+ "foreignKeys": {},
123+ "compositePrimaryKeys": {
124+ "definition_scope_id_id_pk": {
125+ "columns": [
126+ "scope_id",
127+ "id"
128+ ],
129+ "name": "definition_scope_id_id_pk"
130+ }
131+ },
132+ "uniqueConstraints": {},
133+ "checkConstraints": {}
134+ },
135+ "google_discovery_binding": {
136+ "name": "google_discovery_binding",
137+ "columns": {
138+ "id": {
139+ "name": "id",
140+ "type": "text",
141+ "primaryKey": false,
142+ "notNull": true,
143+ "autoincrement": false
144+ },
145+ "scope_id": {
146+ "name": "scope_id",
147+ "type": "text",
148+ "primaryKey": false,
149+ "notNull": true,
150+ "autoincrement": false
151+ },
152+ "source_id": {
153+ "name": "source_id",
154+ "type": "text",
155+ "primaryKey": false,
156+ "notNull": true,
157+ "autoincrement": false
158+ },
159+ "binding": {
160+ "name": "binding",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": true,
164+ "autoincrement": false
165+ },
166+ "created_at": {
167+ "name": "created_at",
168+ "type": "integer",
169+ "primaryKey": false,
170+ "notNull": true,
171+ "autoincrement": false
172+ }
173+ },
174+ "indexes": {
175+ "google_discovery_binding_scope_id_idx": {
176+ "name": "google_discovery_binding_scope_id_idx",
177+ "columns": [
178+ "scope_id"
179+ ],
180+ "isUnique": false
181+ },
182+ "google_discovery_binding_source_id_idx": {
183+ "name": "google_discovery_binding_source_id_idx",
184+ "columns": [
185+ "source_id"
186+ ],
187+ "isUnique": false
188+ }
189+ },
190+ "foreignKeys": {},
191+ "compositePrimaryKeys": {
192+ "google_discovery_binding_scope_id_id_pk": {
193+ "columns": [
194+ "scope_id",
195+ "id"
196+ ],
197+ "name": "google_discovery_binding_scope_id_id_pk"
198+ }
199+ },
200+ "uniqueConstraints": {},
201+ "checkConstraints": {}
202+ },
203+ "google_discovery_oauth_session": {
204+ "name": "google_discovery_oauth_session",
205+ "columns": {
206+ "id": {
207+ "name": "id",
208+ "type": "text",
209+ "primaryKey": false,
210+ "notNull": true,
211+ "autoincrement": false
212+ },
213+ "scope_id": {
214+ "name": "scope_id",
215+ "type": "text",
216+ "primaryKey": false,
217+ "notNull": true,
218+ "autoincrement": false
219+ },
220+ "session": {
221+ "name": "session",
222+ "type": "text",
223+ "primaryKey": false,
224+ "notNull": true,
225+ "autoincrement": false
226+ },
227+ "expires_at": {
228+ "name": "expires_at",
229+ "type": "integer",
230+ "primaryKey": false,
231+ "notNull": true,
232+ "autoincrement": false
233+ }
234+ },
235+ "indexes": {
236+ "google_discovery_oauth_session_scope_id_idx": {
237+ "name": "google_discovery_oauth_session_scope_id_idx",
238+ "columns": [
239+ "scope_id"
240+ ],
241+ "isUnique": false
242+ }
243+ },
244+ "foreignKeys": {},
245+ "compositePrimaryKeys": {
246+ "google_discovery_oauth_session_scope_id_id_pk": {
247+ "columns": [
248+ "scope_id",
249+ "id"
250+ ],
251+ "name": "google_discovery_oauth_session_scope_id_id_pk"
252+ }
253+ },
254+ "uniqueConstraints": {},
255+ "checkConstraints": {}
256+ },
257+ "google_discovery_source": {
258+ "name": "google_discovery_source",
259+ "columns": {
260+ "id": {
261+ "name": "id",
262+ "type": "text",
263+ "primaryKey": false,
264+ "notNull": true,
265+ "autoincrement": false
266+ },
267+ "scope_id": {
268+ "name": "scope_id",
269+ "type": "text",
270+ "primaryKey": false,
271+ "notNull": true,
272+ "autoincrement": false
273+ },
274+ "name": {
275+ "name": "name",
276+ "type": "text",
277+ "primaryKey": false,
278+ "notNull": true,
279+ "autoincrement": false
280+ },
281+ "config": {
282+ "name": "config",
283+ "type": "text",
284+ "primaryKey": false,
285+ "notNull": true,
286+ "autoincrement": false
287+ },
288+ "created_at": {
289+ "name": "created_at",
290+ "type": "integer",
291+ "primaryKey": false,
292+ "notNull": true,
293+ "autoincrement": false
294+ },
295+ "updated_at": {
296+ "name": "updated_at",
297+ "type": "integer",
298+ "primaryKey": false,
299+ "notNull": true,
300+ "autoincrement": false
301+ }
302+ },
303+ "indexes": {
304+ "google_discovery_source_scope_id_idx": {
305+ "name": "google_discovery_source_scope_id_idx",
306+ "columns": [
307+ "scope_id"
308+ ],
309+ "isUnique": false
310+ }
311+ },
312+ "foreignKeys": {},
313+ "compositePrimaryKeys": {
314+ "google_discovery_source_scope_id_id_pk": {
315+ "columns": [
316+ "scope_id",
317+ "id"
318+ ],
319+ "name": "google_discovery_source_scope_id_id_pk"
320+ }
321+ },
322+ "uniqueConstraints": {},
323+ "checkConstraints": {}
324+ },
325+ "graphql_operation": {
326+ "name": "graphql_operation",
327+ "columns": {
328+ "id": {
329+ "name": "id",
330+ "type": "text",
331+ "primaryKey": false,
332+ "notNull": true,
333+ "autoincrement": false
334+ },
335+ "scope_id": {
336+ "name": "scope_id",
337+ "type": "text",
338+ "primaryKey": false,
339+ "notNull": true,
340+ "autoincrement": false
341+ },
342+ "source_id": {
343+ "name": "source_id",
344+ "type": "text",
345+ "primaryKey": false,
346+ "notNull": true,
347+ "autoincrement": false
348+ },
349+ "binding": {
350+ "name": "binding",
351+ "type": "text",
352+ "primaryKey": false,
353+ "notNull": true,
354+ "autoincrement": false
355+ }
356+ },
357+ "indexes": {
358+ "graphql_operation_scope_id_idx": {
359+ "name": "graphql_operation_scope_id_idx",
360+ "columns": [
361+ "scope_id"
362+ ],
363+ "isUnique": false
364+ },
365+ "graphql_operation_source_id_idx": {
366+ "name": "graphql_operation_source_id_idx",
367+ "columns": [
368+ "source_id"
369+ ],
370+ "isUnique": false
371+ }
372+ },
373+ "foreignKeys": {},
374+ "compositePrimaryKeys": {
375+ "graphql_operation_scope_id_id_pk": {
376+ "columns": [
377+ "scope_id",
378+ "id"
379+ ],
380+ "name": "graphql_operation_scope_id_id_pk"
381+ }
382+ },
383+ "uniqueConstraints": {},
384+ "checkConstraints": {}
385+ },
386+ "graphql_source": {
387+ "name": "graphql_source",
388+ "columns": {
389+ "id": {
390+ "name": "id",
391+ "type": "text",
392+ "primaryKey": false,
393+ "notNull": true,
394+ "autoincrement": false
395+ },
396+ "scope_id": {
397+ "name": "scope_id",
398+ "type": "text",
399+ "primaryKey": false,
400+ "notNull": true,
401+ "autoincrement": false
402+ },
403+ "name": {
404+ "name": "name",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": true,
408+ "autoincrement": false
409+ },
410+ "endpoint": {
411+ "name": "endpoint",
412+ "type": "text",
413+ "primaryKey": false,
414+ "notNull": true,
415+ "autoincrement": false
416+ },
417+ "headers": {
418+ "name": "headers",
419+ "type": "text",
420+ "primaryKey": false,
421+ "notNull": false,
422+ "autoincrement": false
423+ }
424+ },
425+ "indexes": {
426+ "graphql_source_scope_id_idx": {
427+ "name": "graphql_source_scope_id_idx",
428+ "columns": [
429+ "scope_id"
430+ ],
431+ "isUnique": false
432+ }
433+ },
434+ "foreignKeys": {},
435+ "compositePrimaryKeys": {
436+ "graphql_source_scope_id_id_pk": {
437+ "columns": [
438+ "scope_id",
439+ "id"
440+ ],
441+ "name": "graphql_source_scope_id_id_pk"
442+ }
443+ },
444+ "uniqueConstraints": {},
445+ "checkConstraints": {}
446+ },
447+ "mcp_binding": {
448+ "name": "mcp_binding",
449+ "columns": {
450+ "id": {
451+ "name": "id",
452+ "type": "text",
453+ "primaryKey": false,
454+ "notNull": true,
455+ "autoincrement": false
456+ },
457+ "scope_id": {
458+ "name": "scope_id",
459+ "type": "text",
460+ "primaryKey": false,
461+ "notNull": true,
462+ "autoincrement": false
463+ },
464+ "source_id": {
465+ "name": "source_id",
466+ "type": "text",
467+ "primaryKey": false,
468+ "notNull": true,
469+ "autoincrement": false
470+ },
471+ "binding": {
472+ "name": "binding",
473+ "type": "text",
474+ "primaryKey": false,
475+ "notNull": true,
476+ "autoincrement": false
477+ },
478+ "created_at": {
479+ "name": "created_at",
480+ "type": "integer",
481+ "primaryKey": false,
482+ "notNull": true,
483+ "autoincrement": false
484+ }
485+ },
486+ "indexes": {
487+ "mcp_binding_scope_id_idx": {
488+ "name": "mcp_binding_scope_id_idx",
489+ "columns": [
490+ "scope_id"
491+ ],
492+ "isUnique": false
493+ },
494+ "mcp_binding_source_id_idx": {
495+ "name": "mcp_binding_source_id_idx",
496+ "columns": [
497+ "source_id"
498+ ],
499+ "isUnique": false
500+ }
501+ },
502+ "foreignKeys": {},
503+ "compositePrimaryKeys": {
504+ "mcp_binding_scope_id_id_pk": {
505+ "columns": [
506+ "scope_id",
507+ "id"
508+ ],
509+ "name": "mcp_binding_scope_id_id_pk"
510+ }
511+ },
512+ "uniqueConstraints": {},
513+ "checkConstraints": {}
514+ },
515+ "mcp_oauth_session": {
516+ "name": "mcp_oauth_session",
517+ "columns": {
518+ "id": {
519+ "name": "id",
520+ "type": "text",
521+ "primaryKey": false,
522+ "notNull": true,
523+ "autoincrement": false
524+ },
525+ "scope_id": {
526+ "name": "scope_id",
527+ "type": "text",
528+ "primaryKey": false,
529+ "notNull": true,
530+ "autoincrement": false
531+ },
532+ "session": {
533+ "name": "session",
534+ "type": "text",
535+ "primaryKey": false,
536+ "notNull": true,
537+ "autoincrement": false
538+ },
539+ "expires_at": {
540+ "name": "expires_at",
541+ "type": "integer",
542+ "primaryKey": false,
543+ "notNull": true,
544+ "autoincrement": false
545+ },
546+ "created_at": {
547+ "name": "created_at",
548+ "type": "integer",
549+ "primaryKey": false,
550+ "notNull": true,
551+ "autoincrement": false
552+ }
553+ },
554+ "indexes": {
555+ "mcp_oauth_session_scope_id_idx": {
556+ "name": "mcp_oauth_session_scope_id_idx",
557+ "columns": [
558+ "scope_id"
559+ ],
560+ "isUnique": false
561+ }
562+ },
563+ "foreignKeys": {},
564+ "compositePrimaryKeys": {
565+ "mcp_oauth_session_scope_id_id_pk": {
566+ "columns": [
567+ "scope_id",
568+ "id"
569+ ],
570+ "name": "mcp_oauth_session_scope_id_id_pk"
571+ }
572+ },
573+ "uniqueConstraints": {},
574+ "checkConstraints": {}
575+ },
576+ "mcp_source": {
577+ "name": "mcp_source",
578+ "columns": {
579+ "id": {
580+ "name": "id",
581+ "type": "text",
582+ "primaryKey": false,
583+ "notNull": true,
584+ "autoincrement": false
585+ },
586+ "scope_id": {
587+ "name": "scope_id",
588+ "type": "text",
589+ "primaryKey": false,
590+ "notNull": true,
591+ "autoincrement": false
592+ },
593+ "name": {
594+ "name": "name",
595+ "type": "text",
596+ "primaryKey": false,
597+ "notNull": true,
598+ "autoincrement": false
599+ },
600+ "config": {
601+ "name": "config",
602+ "type": "text",
603+ "primaryKey": false,
604+ "notNull": true,
605+ "autoincrement": false
606+ },
607+ "created_at": {
608+ "name": "created_at",
609+ "type": "integer",
610+ "primaryKey": false,
611+ "notNull": true,
612+ "autoincrement": false
613+ }
614+ },
615+ "indexes": {
616+ "mcp_source_scope_id_idx": {
617+ "name": "mcp_source_scope_id_idx",
618+ "columns": [
619+ "scope_id"
620+ ],
621+ "isUnique": false
622+ }
623+ },
624+ "foreignKeys": {},
625+ "compositePrimaryKeys": {
626+ "mcp_source_scope_id_id_pk": {
627+ "columns": [
628+ "scope_id",
629+ "id"
630+ ],
631+ "name": "mcp_source_scope_id_id_pk"
632+ }
633+ },
634+ "uniqueConstraints": {},
635+ "checkConstraints": {}
636+ },
637+ "openapi_oauth_session": {
638+ "name": "openapi_oauth_session",
639+ "columns": {
640+ "id": {
641+ "name": "id",
642+ "type": "text",
643+ "primaryKey": false,
644+ "notNull": true,
645+ "autoincrement": false
646+ },
647+ "scope_id": {
648+ "name": "scope_id",
649+ "type": "text",
650+ "primaryKey": false,
651+ "notNull": true,
652+ "autoincrement": false
653+ },
654+ "session": {
655+ "name": "session",
656+ "type": "text",
657+ "primaryKey": false,
658+ "notNull": true,
659+ "autoincrement": false
660+ },
661+ "created_at": {
662+ "name": "created_at",
663+ "type": "integer",
664+ "primaryKey": false,
665+ "notNull": true,
666+ "autoincrement": false
667+ }
668+ },
669+ "indexes": {
670+ "openapi_oauth_session_scope_id_idx": {
671+ "name": "openapi_oauth_session_scope_id_idx",
672+ "columns": [
673+ "scope_id"
674+ ],
675+ "isUnique": false
676+ }
677+ },
678+ "foreignKeys": {},
679+ "compositePrimaryKeys": {
680+ "openapi_oauth_session_scope_id_id_pk": {
681+ "columns": [
682+ "scope_id",
683+ "id"
684+ ],
685+ "name": "openapi_oauth_session_scope_id_id_pk"
686+ }
687+ },
688+ "uniqueConstraints": {},
689+ "checkConstraints": {}
690+ },
691+ "openapi_operation": {
692+ "name": "openapi_operation",
693+ "columns": {
694+ "id": {
695+ "name": "id",
696+ "type": "text",
697+ "primaryKey": false,
698+ "notNull": true,
699+ "autoincrement": false
700+ },
701+ "scope_id": {
702+ "name": "scope_id",
703+ "type": "text",
704+ "primaryKey": false,
705+ "notNull": true,
706+ "autoincrement": false
707+ },
708+ "source_id": {
709+ "name": "source_id",
710+ "type": "text",
711+ "primaryKey": false,
712+ "notNull": true,
713+ "autoincrement": false
714+ },
715+ "binding": {
716+ "name": "binding",
717+ "type": "text",
718+ "primaryKey": false,
719+ "notNull": true,
720+ "autoincrement": false
721+ }
722+ },
723+ "indexes": {
724+ "openapi_operation_scope_id_idx": {
725+ "name": "openapi_operation_scope_id_idx",
726+ "columns": [
727+ "scope_id"
728+ ],
729+ "isUnique": false
730+ },
731+ "openapi_operation_source_id_idx": {
732+ "name": "openapi_operation_source_id_idx",
733+ "columns": [
734+ "source_id"
735+ ],
736+ "isUnique": false
737+ }
738+ },
739+ "foreignKeys": {},
740+ "compositePrimaryKeys": {
741+ "openapi_operation_scope_id_id_pk": {
742+ "columns": [
743+ "scope_id",
744+ "id"
745+ ],
746+ "name": "openapi_operation_scope_id_id_pk"
747+ }
748+ },
749+ "uniqueConstraints": {},
750+ "checkConstraints": {}
751+ },
752+ "openapi_source": {
753+ "name": "openapi_source",
754+ "columns": {
755+ "id": {
756+ "name": "id",
757+ "type": "text",
758+ "primaryKey": false,
759+ "notNull": true,
760+ "autoincrement": false
761+ },
762+ "scope_id": {
763+ "name": "scope_id",
764+ "type": "text",
765+ "primaryKey": false,
766+ "notNull": true,
767+ "autoincrement": false
768+ },
769+ "name": {
770+ "name": "name",
771+ "type": "text",
772+ "primaryKey": false,
773+ "notNull": true,
774+ "autoincrement": false
775+ },
776+ "spec": {
777+ "name": "spec",
778+ "type": "text",
779+ "primaryKey": false,
780+ "notNull": true,
781+ "autoincrement": false
782+ },
783+ "source_url": {
784+ "name": "source_url",
785+ "type": "text",
786+ "primaryKey": false,
787+ "notNull": false,
788+ "autoincrement": false
789+ },
790+ "base_url": {
791+ "name": "base_url",
792+ "type": "text",
793+ "primaryKey": false,
794+ "notNull": false,
795+ "autoincrement": false
796+ },
797+ "headers": {
798+ "name": "headers",
799+ "type": "text",
800+ "primaryKey": false,
801+ "notNull": false,
802+ "autoincrement": false
803+ },
804+ "oauth2": {
805+ "name": "oauth2",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false,
809+ "autoincrement": false
810+ },
811+ "invocation_config": {
812+ "name": "invocation_config",
813+ "type": "text",
814+ "primaryKey": false,
815+ "notNull": true,
816+ "autoincrement": false
817+ }
818+ },
819+ "indexes": {
820+ "openapi_source_scope_id_idx": {
821+ "name": "openapi_source_scope_id_idx",
822+ "columns": [
823+ "scope_id"
824+ ],
825+ "isUnique": false
826+ }
827+ },
828+ "foreignKeys": {},
829+ "compositePrimaryKeys": {
830+ "openapi_source_scope_id_id_pk": {
831+ "columns": [
832+ "scope_id",
833+ "id"
834+ ],
835+ "name": "openapi_source_scope_id_id_pk"
836+ }
837+ },
838+ "uniqueConstraints": {},
839+ "checkConstraints": {}
840+ },
841+ "secret": {
842+ "name": "secret",
843+ "columns": {
844+ "id": {
845+ "name": "id",
846+ "type": "text",
847+ "primaryKey": false,
848+ "notNull": true,
849+ "autoincrement": false
850+ },
851+ "scope_id": {
852+ "name": "scope_id",
853+ "type": "text",
854+ "primaryKey": false,
855+ "notNull": true,
856+ "autoincrement": false
857+ },
858+ "name": {
859+ "name": "name",
860+ "type": "text",
861+ "primaryKey": false,
862+ "notNull": true,
863+ "autoincrement": false
864+ },
865+ "provider": {
866+ "name": "provider",
867+ "type": "text",
868+ "primaryKey": false,
869+ "notNull": true,
870+ "autoincrement": false
871+ },
872+ "created_at": {
873+ "name": "created_at",
874+ "type": "integer",
875+ "primaryKey": false,
876+ "notNull": true,
877+ "autoincrement": false
878+ }
879+ },
880+ "indexes": {
881+ "secret_scope_id_idx": {
882+ "name": "secret_scope_id_idx",
883+ "columns": [
884+ "scope_id"
885+ ],
886+ "isUnique": false
887+ },
888+ "secret_provider_idx": {
889+ "name": "secret_provider_idx",
890+ "columns": [
891+ "provider"
892+ ],
893+ "isUnique": false
894+ }
895+ },
896+ "foreignKeys": {},
897+ "compositePrimaryKeys": {
898+ "secret_scope_id_id_pk": {
899+ "columns": [
900+ "scope_id",
901+ "id"
902+ ],
903+ "name": "secret_scope_id_id_pk"
904+ }
905+ },
906+ "uniqueConstraints": {},
907+ "checkConstraints": {}
908+ },
909+ "source": {
910+ "name": "source",
911+ "columns": {
912+ "id": {
913+ "name": "id",
914+ "type": "text",
915+ "primaryKey": false,
916+ "notNull": true,
917+ "autoincrement": false
918+ },
919+ "scope_id": {
920+ "name": "scope_id",
921+ "type": "text",
922+ "primaryKey": false,
923+ "notNull": true,
924+ "autoincrement": false
925+ },
926+ "plugin_id": {
927+ "name": "plugin_id",
928+ "type": "text",
929+ "primaryKey": false,
930+ "notNull": true,
931+ "autoincrement": false
932+ },
933+ "kind": {
934+ "name": "kind",
935+ "type": "text",
936+ "primaryKey": false,
937+ "notNull": true,
938+ "autoincrement": false
939+ },
940+ "name": {
941+ "name": "name",
942+ "type": "text",
943+ "primaryKey": false,
944+ "notNull": true,
945+ "autoincrement": false
946+ },
947+ "url": {
948+ "name": "url",
949+ "type": "text",
950+ "primaryKey": false,
951+ "notNull": false,
952+ "autoincrement": false
953+ },
954+ "can_remove": {
955+ "name": "can_remove",
956+ "type": "integer",
957+ "primaryKey": false,
958+ "notNull": true,
959+ "autoincrement": false,
960+ "default": true
961+ },
962+ "can_refresh": {
963+ "name": "can_refresh",
964+ "type": "integer",
965+ "primaryKey": false,
966+ "notNull": true,
967+ "autoincrement": false,
968+ "default": false
969+ },
970+ "can_edit": {
971+ "name": "can_edit",
972+ "type": "integer",
973+ "primaryKey": false,
974+ "notNull": true,
975+ "autoincrement": false,
976+ "default": false
977+ },
978+ "created_at": {
979+ "name": "created_at",
980+ "type": "integer",
981+ "primaryKey": false,
982+ "notNull": true,
983+ "autoincrement": false
984+ },
985+ "updated_at": {
986+ "name": "updated_at",
987+ "type": "integer",
988+ "primaryKey": false,
989+ "notNull": true,
990+ "autoincrement": false
991+ }
992+ },
993+ "indexes": {
994+ "source_scope_id_idx": {
995+ "name": "source_scope_id_idx",
996+ "columns": [
997+ "scope_id"
998+ ],
999+ "isUnique": false
1000+ },
1001+ "source_plugin_id_idx": {
1002+ "name": "source_plugin_id_idx",
1003+ "columns": [
1004+ "plugin_id"
1005+ ],
1006+ "isUnique": false
1007+ }
1008+ },
1009+ "foreignKeys": {},
1010+ "compositePrimaryKeys": {
1011+ "source_scope_id_id_pk": {
1012+ "columns": [
1013+ "scope_id",
1014+ "id"
1015+ ],
1016+ "name": "source_scope_id_id_pk"
1017+ }
1018+ },
1019+ "uniqueConstraints": {},
1020+ "checkConstraints": {}
1021+ },
1022+ "tool": {
1023+ "name": "tool",
1024+ "columns": {
1025+ "id": {
1026+ "name": "id",
1027+ "type": "text",
1028+ "primaryKey": false,
1029+ "notNull": true,
1030+ "autoincrement": false
1031+ },
1032+ "scope_id": {
1033+ "name": "scope_id",
1034+ "type": "text",
1035+ "primaryKey": false,
1036+ "notNull": true,
1037+ "autoincrement": false
1038+ },
1039+ "source_id": {
1040+ "name": "source_id",
1041+ "type": "text",
1042+ "primaryKey": false,
1043+ "notNull": true,
1044+ "autoincrement": false
1045+ },
1046+ "plugin_id": {
1047+ "name": "plugin_id",
1048+ "type": "text",
1049+ "primaryKey": false,
1050+ "notNull": true,
1051+ "autoincrement": false
1052+ },
1053+ "name": {
1054+ "name": "name",
1055+ "type": "text",
1056+ "primaryKey": false,
1057+ "notNull": true,
1058+ "autoincrement": false
1059+ },
1060+ "description": {
1061+ "name": "description",
1062+ "type": "text",
1063+ "primaryKey": false,
1064+ "notNull": true,
1065+ "autoincrement": false
1066+ },
1067+ "input_schema": {
1068+ "name": "input_schema",
1069+ "type": "text",
1070+ "primaryKey": false,
1071+ "notNull": false,
1072+ "autoincrement": false
1073+ },
1074+ "output_schema": {
1075+ "name": "output_schema",
1076+ "type": "text",
1077+ "primaryKey": false,
1078+ "notNull": false,
1079+ "autoincrement": false
1080+ },
1081+ "created_at": {
1082+ "name": "created_at",
1083+ "type": "integer",
1084+ "primaryKey": false,
1085+ "notNull": true,
1086+ "autoincrement": false
1087+ },
1088+ "updated_at": {
1089+ "name": "updated_at",
1090+ "type": "integer",
1091+ "primaryKey": false,
1092+ "notNull": true,
1093+ "autoincrement": false
1094+ }
1095+ },
1096+ "indexes": {
1097+ "tool_scope_id_idx": {
1098+ "name": "tool_scope_id_idx",
1099+ "columns": [
1100+ "scope_id"
1101+ ],
1102+ "isUnique": false
1103+ },
1104+ "tool_source_id_idx": {
1105+ "name": "tool_source_id_idx",
1106+ "columns": [
1107+ "source_id"
1108+ ],
1109+ "isUnique": false
1110+ },
1111+ "tool_plugin_id_idx": {
1112+ "name": "tool_plugin_id_idx",
1113+ "columns": [
1114+ "plugin_id"
1115+ ],
1116+ "isUnique": false
1117+ }
1118+ },
1119+ "foreignKeys": {},
1120+ "compositePrimaryKeys": {
1121+ "tool_scope_id_id_pk": {
1122+ "columns": [
1123+ "scope_id",
1124+ "id"
1125+ ],
1126+ "name": "tool_scope_id_id_pk"
1127+ }
1128+ },
1129+ "uniqueConstraints": {},
1130+ "checkConstraints": {}
1131+ }
1132+ },
1133+ "views": {},
1134+ "enums": {},
1135+ "_meta": {
1136+ "schemas": {},
1137+ "tables": {},
1138+ "columns": {}
1139+ },
1140+ "internal": {
1141+ "indexes": {}
1142+ }
1143+}
apps/local/drizzle/meta/_journal.json+7−0
@@ -8,6 +8,13 @@ · 7 unmodified lines
88 "when": 1776367901699,
99 "tag": "0000_overconfident_sharon_carter",
1010 "breakpoints": true
11+ },
12+ {
13+ "idx": 1,
14+ "version": "6",
15+ "when": 1776680432025,
16+ "tag": "0001_sour_catseye",
17+ "breakpoints": true
1118 }
1219 ]
1320 }
apps/local/src/server/executor-schema.ts+1−3
@@ -68,6 +68,7 @@ export const openapi_source = sqliteTable("openapi_source", { · 67 unmodified lines
6868 scope_id: text('scope_id').notNull(),
6969 name: text('name').notNull(),
7070 spec: text('spec').notNull(),
71+ source_url: text('source_url'),
7172 base_url: text('base_url'),
7273 headers: text('headers', { mode: "json" }),
7374 oauth2: text('oauth2', { mode: "json" }),
@@ -188,9 +189,6 @@ export const graphql_operation = sqliteTable("graphql_operation", { · 114 unmodified lines
188189 index("graphql_operation_source_id_idx").on(table.source_id),
189190 ]);
190191
191−// Blob store table — hand-appended. BlobStore is a separate storage
192−// abstraction from DBSchema, so the CLI doesn't generate it. Keep in
193−// sync with @executor/storage-file's BlobStore implementation.
194192 export const blob = sqliteTable("blob", {
195193 namespace: text('namespace').notNull(),
196194 key: text('key').notNull(),
apps/local/src/server/mcp-oauth.test.ts+458−0
@@ -0,0 +1,458 @@
1+// ---------------------------------------------------------------------------
2+// Local app × MCP OAuth — real HTTP end-to-end
3+// ---------------------------------------------------------------------------
4+//
5+// Mirrors apps/cloud/src/services/mcp-oauth.node.test.ts but for the local
6+// (sqlite) server. Drives the real LocalApi (core + mcp groups) against a
7+// real in-process OAuth + MCP server. Every layer between the test and the
8+// plugin is real:
9+//
10+// test → HttpApiClient → in-process webHandler → LocalApi
11+// → McpHandlers → mcpPlugin.startOAuth / completeOAuth
12+// → MCP SDK `auth()`
13+// → fake OAuth server (DCR, /authorize → 302, /token, AS metadata,
14+// protected resource metadata)
15+//
16+// Single-scope: local has one scope per project (`${folder}-${hash}`) so
17+// the OAuth flow lands tokens at that scope and `secrets.resolve` reads
18+// them back through the same provider (file-secrets in a tmpdir).
19+// ---------------------------------------------------------------------------
20+
21+import { afterAll, beforeAll, describe, expect, it } from "vitest";
22+import { createServer, type Server } from "node:http";
23+import type { AddressInfo } from "node:net";
24+import { createHash, randomBytes } from "node:crypto";
25+import { mkdtempSync, rmSync } from "node:fs";
26+import { tmpdir } from "node:os";
27+import { join } from "node:path";
28+import { Database } from "bun:sqlite";
29+import { drizzle } from "drizzle-orm/bun-sqlite";
30+import { migrate } from "drizzle-orm/bun-sqlite/migrator";
31+
32+import {
33+ FetchHttpClient,
34+ HttpApi,
35+ HttpApiBuilder,
36+ HttpApiClient,
37+ HttpMiddleware,
38+ HttpRouter,
39+ HttpServer,
40+} from "@effect/platform";
41+import { Effect, Layer } from "effect";
42+
43+import {
44+ addGroup,
45+ InternalError,
46+ observabilityMiddleware,
47+} from "@executor/api";
48+import {
49+ CoreHandlers,
50+ ExecutionEngineService,
51+ ExecutorService,
52+} from "@executor/api/server";
53+import { createExecutionEngine } from "@executor/execution";
54+import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
55+import {
56+ Scope,
57+ ScopeId,
58+ SecretId,
59+ collectSchemas,
60+ createExecutor,
61+} from "@executor/sdk";
62+import {
63+ makeSqliteAdapter,
64+ makeSqliteBlobStore,
65+} from "@executor/storage-file";
66+import { fileSecretsPlugin } from "@executor/plugin-file-secrets";
67+import { mcpPlugin } from "@executor/plugin-mcp";
68+import {
69+ McpExtensionService,
70+ McpGroup,
71+ McpHandlers,
72+} from "@executor/plugin-mcp/api";
73+
74+import * as executorSchema from "./executor-schema";
75+import { ErrorCaptureLive } from "./observability";
76+
77+// Shape of the test API: core + mcp group, with InternalError surfaced at
78+// the top level so `observabilityMiddleware` can land its typed-error
79+// bridge on every endpoint.
80+const TestApi = addGroup(McpGroup).addError(InternalError);
81+type TestApiShape = typeof TestApi extends HttpApi.HttpApi<
82+ infer _Id,
83+ infer Groups,
84+ infer ApiError,
85+ infer _ApiR
86+>
87+ ? HttpApiClient.Client<Groups, ApiError, never>
88+ : never;
89+
90+// ---------------------------------------------------------------------------
91+// Fake OAuth + MCP server (mirrors the cloud test)
92+// ---------------------------------------------------------------------------
93+
94+interface FakeServer {
95+ readonly url: string;
96+ readonly close: () => Promise<void>;
97+}
98+
99+const startFakeServer = async (): Promise<FakeServer> => {
100+ const clients = new Map<string, { redirect_uris: readonly string[] }>();
101+ const codes = new Map<
102+ string,
103+ { readonly clientId: string; readonly codeChallenge: string }
104+ >();
105+ const accessTokens = new Map<string, { readonly refresh: string }>();
106+ const refreshTokens = new Map<string, string>();
107+ let seq = 0;
108+ const next = (p: string) =>
109+ `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
110+
111+ const readBody = (req: import("node:http").IncomingMessage): Promise<string> =>
112+ new Promise((resolve, reject) => {
113+ let buf = "";
114+ req.on("data", (chunk) => (buf += chunk));
115+ req.on("end", () => resolve(buf));
116+ req.on("error", reject);
117+ });
118+
119+ const server: Server = createServer(async (req, res) => {
120+ const url = new URL(req.url!, `http://${req.headers.host}`);
121+ const send = (
122+ status: number,
123+ body: unknown,
124+ headers: Record<string, string> = {},
125+ ) => {
126+ const payload = typeof body === "string" ? body : JSON.stringify(body);
127+ res.writeHead(status, {
128+ "content-type":
129+ typeof body === "string" ? "text/plain" : "application/json",
130+ ...headers,
131+ });
132+ res.end(payload);
133+ };
134+
135+ try {
136+ if (url.pathname === "/.well-known/oauth-protected-resource") {
137+ const origin = `http://${req.headers.host}`;
138+ return send(200, {
139+ resource: origin,
140+ authorization_servers: [origin],
141+ bearer_methods_supported: ["header"],
142+ });
143+ }
144+
145+ if (url.pathname === "/.well-known/oauth-authorization-server") {
146+ const issuer = `http://${req.headers.host}`;
147+ return send(200, {
148+ issuer,
149+ authorization_endpoint: `${issuer}/authorize`,
150+ token_endpoint: `${issuer}/token`,
151+ registration_endpoint: `${issuer}/register`,
152+ response_types_supported: ["code"],
153+ grant_types_supported: ["authorization_code", "refresh_token"],
154+ code_challenge_methods_supported: ["S256"],
155+ token_endpoint_auth_methods_supported: ["none"],
156+ });
157+ }
158+
159+ if (url.pathname === "/register" && req.method === "POST") {
160+ const body = await readBody(req);
161+ const parsed = JSON.parse(body) as {
162+ readonly redirect_uris?: readonly string[];
163+ readonly grant_types?: readonly string[];
164+ readonly response_types?: readonly string[];
165+ };
166+ const clientId = next("client");
167+ clients.set(clientId, { redirect_uris: parsed.redirect_uris ?? [] });
168+ return send(201, {
169+ client_id: clientId,
170+ client_id_issued_at: Math.floor(Date.now() / 1000),
171+ redirect_uris: parsed.redirect_uris ?? [],
172+ grant_types: parsed.grant_types ?? [
173+ "authorization_code",
174+ "refresh_token",
175+ ],
176+ response_types: parsed.response_types ?? ["code"],
177+ token_endpoint_auth_method: "none",
178+ });
179+ }
180+
181+ if (url.pathname === "/authorize" && req.method === "GET") {
182+ const clientId = url.searchParams.get("client_id") ?? "";
183+ const redirectUri = url.searchParams.get("redirect_uri") ?? "";
184+ const state = url.searchParams.get("state") ?? "";
185+ const codeChallenge = url.searchParams.get("code_challenge") ?? "";
186+ const method = url.searchParams.get("code_challenge_method") ?? "";
187+ if (!clients.has(clientId)) {
188+ return send(400, { error: "unknown_client" });
189+ }
190+ if (method !== "S256" || !codeChallenge) {
191+ return send(400, { error: "invalid_request" });
192+ }
193+ const code = next("code");
194+ codes.set(code, { clientId, codeChallenge });
195+ const destination = new URL(redirectUri);
196+ destination.searchParams.set("code", code);
197+ if (state) destination.searchParams.set("state", state);
198+ return send(302, "", { location: destination.toString() });
199+ }
200+
201+ if (url.pathname === "/token" && req.method === "POST") {
202+ const body = await readBody(req);
203+ const params = new URLSearchParams(body);
204+ const grant = params.get("grant_type");
205+
206+ if (grant === "authorization_code") {
207+ const code = params.get("code") ?? "";
208+ const verifier = params.get("code_verifier") ?? "";
209+ const record = codes.get(code);
210+ if (!record) return send(400, { error: "invalid_grant" });
211+ codes.delete(code);
212+ const computed = createHash("sha256")
213+ .update(verifier)
214+ .digest("base64url");
215+ if (computed !== record.codeChallenge) {
216+ return send(400, { error: "invalid_grant" });
217+ }
218+ const access = next("at");
219+ const refresh = next("rt");
220+ accessTokens.set(access, { refresh });
221+ refreshTokens.set(refresh, access);
222+ return send(200, {
223+ access_token: access,
224+ refresh_token: refresh,
225+ token_type: "Bearer",
226+ expires_in: 3600,
227+ });
228+ }
229+
230+ return send(400, { error: "unsupported_grant_type" });
231+ }
232+
233+ if (url.pathname === "/mcp") {
234+ const origin = `http://${req.headers.host}`;
235+ return send(
236+ 401,
237+ { error: "unauthorized" },
238+ {
239+ "www-authenticate": `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource"`,
240+ },
241+ );
242+ }
243+
244+ send(404, { error: "not_found", path: url.pathname });
245+ } catch (e) {
246+ send(500, { error: "server_error", message: String(e) });
247+ }
248+ });
249+
250+ await new Promise<void>((resolve) =>
251+ server.listen(0, "127.0.0.1", () => resolve()),
252+ );
253+ const address = server.address() as AddressInfo;
254+
255+ return {
256+ url: `http://127.0.0.1:${address.port}`,
257+ close: () => new Promise<void>((resolve) => server.close(() => resolve())),
258+ };
259+};
260+
261+// ---------------------------------------------------------------------------
262+// In-process local API harness — tmpdir sqlite + minimal plugin set.
263+// ---------------------------------------------------------------------------
264+
265+const TEST_BASE_URL = "http://local.test";
266+const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");
267+
268+interface Harness {
269+ readonly fetch: typeof globalThis.fetch;
270+ readonly scopeId: string;
271+ readonly dispose: () => Promise<void>;
272+}
273+
274+const startHarness = async (tmpDir: string): Promise<Harness> => {
275+ const sqlite = new Database(join(tmpDir, "data.db"));
276+ sqlite.exec("PRAGMA journal_mode = WAL");
277+ const db = drizzle(sqlite, { schema: executorSchema });
278+ migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
279+
280+ const scopeId = `test-${randomBytes(4).toString("hex")}`;
281+ const plugins = [
282+ mcpPlugin({ dangerouslyAllowStdioMCP: false }),
283+ fileSecretsPlugin({ directory: tmpDir }),
284+ ] as const;
285+ const schema = collectSchemas(plugins);
286+ const adapter = makeSqliteAdapter({ db, schema });
287+ const blobs = makeSqliteBlobStore({ db });
288+
289+ const scope = new Scope({
290+ id: ScopeId.make(scopeId),
291+ name: "test",
292+ createdAt: new Date(),
293+ });
294+
295+ const executor = await Effect.runPromise(
296+ createExecutor({ scopes: [scope], adapter, blobs, plugins }),
297+ );
298+
299+ const engine = createExecutionEngine({
300+ executor,
301+ codeExecutor: makeQuickJsExecutor(),
302+ });
303+
304+ const TestObservability = observabilityMiddleware(TestApi);
305+
306+ const TestApiBase = HttpApiBuilder.api(TestApi).pipe(
307+ Layer.provide(CoreHandlers),
308+ Layer.provide(McpHandlers),
309+ Layer.provide(TestObservability),
310+ Layer.provide(ErrorCaptureLive),
311+ );
312+
313+ const pluginExtensions = Layer.succeed(McpExtensionService, executor.mcp);
314+
315+ const { handler: webHandler, dispose: disposeHandler } =
316+ HttpApiBuilder.toWebHandler(
317+ TestApiBase.pipe(
318+ Layer.provideMerge(pluginExtensions),
319+ Layer.provideMerge(Layer.succeed(ExecutorService, executor)),
320+ Layer.provideMerge(Layer.succeed(ExecutionEngineService, engine)),
321+ Layer.provideMerge(HttpServer.layerContext),
322+ Layer.provideMerge(HttpRouter.setRouterConfig({ maxParamLength: 1000 })),
323+ ),
324+ { middleware: HttpMiddleware.logger },
325+ );
326+
327+ return {
328+ fetch: ((input: RequestInfo | URL, init?: RequestInit) =>
329+ webHandler(
330+ input instanceof Request ? input : new Request(input, init),
331+ )) as typeof globalThis.fetch,
332+ scopeId,
333+ dispose: async () => {
334+ await disposeHandler().catch(() => undefined);
335+ await Effect.runPromise(executor.close()).catch(() => undefined);
336+ sqlite.close();
337+ },
338+ };
339+};
340+
341+// ---------------------------------------------------------------------------
342+// Lifecycle
343+// ---------------------------------------------------------------------------
344+
345+let fake: FakeServer;
346+let tmpDir: string;
347+let harness: Harness;
348+
349+beforeAll(async () => {
350+ fake = await startFakeServer();
351+ tmpDir = mkdtempSync(join(tmpdir(), "executor-local-mcp-"));
352+ harness = await startHarness(tmpDir);
353+});
354+
355+afterAll(async () => {
356+ await harness.dispose();
357+ rmSync(tmpDir, { recursive: true, force: true });
358+ await fake.close();
359+});
360+
361+// ---------------------------------------------------------------------------
362+// Helpers
363+// ---------------------------------------------------------------------------
364+
365+const followAuthorize = async (
366+ authorizationUrl: string,
367+): Promise<{ code: string; state: string }> => {
368+ const response = await fetch(authorizationUrl, { redirect: "manual" });
369+ expect(response.status).toBe(302);
370+ const location = response.headers.get("location");
371+ if (!location) throw new Error("no location header on authorize redirect");
372+ const dest = new URL(location);
373+ const code = dest.searchParams.get("code");
374+ const state = dest.searchParams.get("state");
375+ if (!code || !state)
376+ throw new Error(`redirect missing code/state: ${location}`);
377+ return { code, state };
378+};
379+
380+// ---------------------------------------------------------------------------
381+// Test
382+// ---------------------------------------------------------------------------
383+
384+describe("local mcp oauth (real OAuth + MCP server)", () => {
385+ it("startOAuth → authorize → completeOAuth writes tokens at the scope", async () => {
386+ const clientLayer = FetchHttpClient.layer.pipe(
387+ Layer.provide(Layer.succeed(FetchHttpClient.Fetch, harness.fetch)),
388+ );
389+
390+ const namespace = `ns_${randomBytes(4).toString("hex")}`;
391+ const accessTokenSecretId = `mcp_${namespace}_access_token`;
392+ const refreshTokenSecretId = `mcp_${namespace}_refresh_token`;
393+ const redirectUrl = "http://local.test/api/mcp/oauth/callback";
394+ const scopeId = ScopeId.make(harness.scopeId);
395+
396+ const run = <A, E>(
397+ body: (client: TestApiShape) => Effect.Effect<A, E>,
398+ ): Effect.Effect<A, E> =>
399+ Effect.gen(function* () {
400+ const client = yield* HttpApiClient.make(TestApi, {
401+ baseUrl: TEST_BASE_URL,
402+ });
403+ return yield* body(client);
404+ }).pipe(Effect.provide(clientLayer)) as Effect.Effect<A, E>;
405+
406+ const started = await Effect.runPromise(
407+ run((client) =>
408+ client.mcp.startOAuth({
409+ path: { scopeId },
410+ payload: {
411+ endpoint: `${fake.url}/mcp`,
412+ redirectUrl,
413+ accessTokenSecretId,
414+ refreshTokenSecretId,
415+ },
416+ }),
417+ ),
418+ );
419+ expect(started.sessionId).toMatch(/^mcp_oauth_/);
420+
421+ const { code, state } = await followAuthorize(started.authorizationUrl);
422+ expect(state).toBe(started.sessionId);
423+
424+ const completed = await Effect.runPromise(
425+ run((client) =>
426+ client.mcp.completeOAuth({
427+ path: { scopeId },
428+ payload: { state, code },
429+ }),
430+ ),
431+ );
432+ expect(completed.accessTokenSecretId).toBe(accessTokenSecretId);
433+ expect(completed.refreshTokenSecretId).toBe(refreshTokenSecretId);
434+ expect(completed.tokenType).toBe("Bearer");
435+ expect(completed.clientInformation).not.toBeNull();
436+ expect(completed.authorizationServerUrl).not.toBeNull();
437+
438+ // Tokens landed at the (single) local scope and resolve back through
439+ // the same provider that the plugin wrote them to.
440+ const access = await Effect.runPromise(
441+ run((client) =>
442+ client.secrets.resolve({
443+ path: { scopeId, secretId: SecretId.make(accessTokenSecretId) },
444+ }),
445+ ),
446+ );
447+ expect(access.value).toMatch(/^at_/);
448+
449+ const refresh = await Effect.runPromise(
450+ run((client) =>
451+ client.secrets.resolve({
452+ path: { scopeId, secretId: SecretId.make(refreshTokenSecretId) },
453+ }),
454+ ),
455+ );
456+ expect(refresh.value).toMatch(/^rt_/);
457+ }, 30_000);
458+});
packages/plugins/mcp/src/sdk/presets.ts+8−0
@@ -87,6 +87,14 @@ export const mcpPresets: readonly McpPreset[] = [ · 86 unmodified lines
8787 icon: "https://linear.app/favicon.ico",
8888 featured: true,
8989 },
90+ {
91+ id: "notion",
92+ name: "Notion",
93+ summary: "Databases, pages, blocks, and search via MCP.",
94+ url: "https://mcp.notion.com/mcp",
95+ icon: "https://www.notion.so/favicon.ico",
96+ featured: true,
97+ },
9098 {
9199 id: "sentry",
92100 name: "Sentry",
packages/plugins/openapi/src/sdk/plugin.ts+161−83
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
1818 SetSecretInput,
1919 SourceDetectionResult,
2020 definePlugin,
21+ type PluginCtx,
2122 type StorageFailure,
2223 type ToolAnnotations,
2324 type ToolRow,
@@ -270,10 +271,154 @@ const toOpenApiSourceConfig = ( · 246 unmodified lines
270271 headers: headersToConfigValues(config.headers),
271272 });
272273
274+const isHttpUrl = (s: string): boolean =>
275+ s.startsWith("http://") || s.startsWith("https://");
276+
273277 export const openApiPlugin = definePlugin(
274278 (options?: OpenApiPluginOptions) => {
275279 const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;
276280
281+ type RebuildInput = {
282+ readonly specText: string;
283+ readonly scope: string;
284+ readonly sourceUrl?: string;
285+ readonly name?: string;
286+ readonly baseUrl?: string;
287+ readonly namespace?: string;
288+ readonly headers?: Record<string, HeaderValue>;
289+ readonly oauth2?: OAuth2Auth;
290+ };
291+
292+ // ctx comes from the plugin runtime — the same instance is passed to
293+ // `extension(ctx)` and to every lifecycle hook (`refreshSource`, etc.),
294+ // so helpers parameterised on ctx can be called from either surface.
295+ const rebuildSource = (
296+ ctx: PluginCtx<OpenapiStore>,
297+ input: RebuildInput,
298+ ) =>
299+ Effect.gen(function* () {
300+ const doc = yield* parse(input.specText);
301+ const result = yield* extract(doc);
302+
303+ const namespace =
304+ input.namespace ??
305+ Option.getOrElse(result.title, () => "api")
306+ .toLowerCase()
307+ .replace(/[^a-z0-9]+/g, "_");
308+
309+ const hoistedDefs: Record<string, unknown> = {};
310+ if (doc.components?.schemas) {
311+ for (const [k, v] of Object.entries(doc.components.schemas)) {
312+ hoistedDefs[k] = normalizeOpenApiRefs(v);
313+ }
314+ }
315+
316+ const baseUrl = input.baseUrl ?? resolveBaseUrl(result.servers);
317+ const oauth2 = input.oauth2 ?? undefined;
318+ const invocationConfig = new InvocationConfig({
319+ baseUrl,
320+ headers: input.headers ?? {},
321+ oauth2: oauth2 ? Option.some(oauth2) : Option.none(),
322+ });
323+
324+ const definitions = compileToolDefinitions(result.operations);
325+ const sourceName =
326+ input.name ?? Option.getOrElse(result.title, () => namespace);
327+
328+ const sourceConfig: SourceConfig = {
329+ spec: input.specText,
330+ sourceUrl: input.sourceUrl,
331+ baseUrl: input.baseUrl,
332+ namespace: input.namespace,
333+ headers: input.headers,
334+ oauth2,
335+ };
336+
337+ const storedSource: StoredSource = {
338+ namespace,
339+ scope: input.scope,
340+ name: sourceName,
341+ config: sourceConfig,
342+ invocationConfig,
343+ };
344+
345+ const storedOps: StoredOperation[] = definitions.map((def) => ({
346+ toolId: `${namespace}.${def.toolPath}`,
347+ sourceId: namespace,
348+ binding: toBinding(def),
349+ }));
350+
351+ yield* ctx.transaction(
352+ Effect.gen(function* () {
353+ yield* ctx.storage.upsertSource(storedSource, storedOps);
354+
355+ yield* ctx.core.sources.register({
356+ id: namespace,
357+ scope: input.scope,
358+ kind: "openapi",
359+ name: sourceName,
360+ url: baseUrl || undefined,
361+ canRemove: true,
362+ // `canRefresh` reflects whether we still know the
363+ // origin URL — sources added from raw spec text have
364+ // nothing to re-fetch, so refresh stays disabled.
365+ canRefresh: input.sourceUrl != null,
366+ canEdit: true,
367+ tools: definitions.map((def) => ({
368+ name: def.toolPath,
369+ description: descriptionFor(def),
370+ inputSchema: normalizeOpenApiRefs(
371+ Option.getOrUndefined(def.operation.inputSchema),
372+ ),
373+ outputSchema: normalizeOpenApiRefs(
374+ Option.getOrUndefined(def.operation.outputSchema),
375+ ),
376+ })),
377+ });
378+
379+ if (Object.keys(hoistedDefs).length > 0) {
380+ yield* ctx.core.definitions.register({
381+ sourceId: namespace,
382+ scope: input.scope,
383+ definitions: hoistedDefs,
384+ });
385+ }
386+ }),
387+ );
388+
389+ return { sourceId: namespace, toolCount: definitions.length };
390+ });
391+
392+ // No-op for missing sources and for sources added from raw spec
393+ // text (no URL to re-fetch from). UIs gate the action via
394+ // `canRefresh` on the source row; reaching here without a URL
395+ // means the caller bypassed that gate, so we stay quiet rather
396+ // than surface a 500 through the unwhitelisted error channel.
397+ const refreshSourceInternal = (
398+ ctx: PluginCtx<OpenapiStore>,
399+ sourceId: string,
400+ scope: string,
401+ ) =>
402+ Effect.gen(function* () {
403+ const existing = yield* ctx.storage.getSource(sourceId, scope);
404+ if (!existing) return;
405+ const sourceUrl = existing.config.sourceUrl;
406+ if (!sourceUrl) return;
407+ const specText = yield* resolveSpecText(sourceUrl).pipe(
408+ Effect.provide(httpClientLayer),
409+ );
410+ yield* rebuildSource(ctx, {
411+ specText,
412+ scope,
413+ sourceUrl,
414+ name: existing.name,
415+ baseUrl: existing.config.baseUrl,
416+ namespace: existing.namespace,
417+ headers: existing.config.headers,
418+ oauth2: existing.config.oauth2,
419+ });
420+ });
421+
277422 return {
278423 id: "openapi" as const,
279424 schema: openapiSchema,
@@ -310,92 +455,16 @@ export const openApiPlugin = definePlugin( · 30 unmodified lines
310455 const specText = yield* resolveSpecText(config.spec).pipe(
311456 Effect.provide(httpClientLayer),
312457 );
313− const doc = yield* parse(specText);
314− const result = yield* extract(doc);
315−
316− const namespace =
317− config.namespace ??
318− Option.getOrElse(result.title, () => "api")
319− .toLowerCase()
320− .replace(/[^a-z0-9]+/g, "_");
321−
322− const hoistedDefs: Record<string, unknown> = {};
323− if (doc.components?.schemas) {
324− for (const [k, v] of Object.entries(doc.components.schemas)) {
325− hoistedDefs[k] = normalizeOpenApiRefs(v);
326− }
327− }
328−
329− const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers);
330− const oauth2 = config.oauth2 ?? undefined;
331− const invocationConfig = new InvocationConfig({
332− baseUrl,
333− headers: config.headers ?? {},
334− oauth2: oauth2 ? Option.some(oauth2) : Option.none(),
335− });
336−
337− const definitions = compileToolDefinitions(result.operations);
338− const sourceName =
339− config.name ?? Option.getOrElse(result.title, () => namespace);
340−
341− const sourceConfig: SourceConfig = {
342− spec: config.spec,
458+ return yield* rebuildSource(ctx, {
459+ specText,
460+ scope: config.scope,
461+ sourceUrl: isHttpUrl(config.spec) ? config.spec : undefined,
462+ name: config.name,
343463 baseUrl: config.baseUrl,
344464 namespace: config.namespace,
345465 headers: config.headers,
346− oauth2,
347− };
348−
349− const storedSource: StoredSource = {
350− namespace,
351− scope: config.scope,
352− name: sourceName,
353− config: sourceConfig,
354− invocationConfig,
355− };
356−
357− const storedOps: StoredOperation[] = definitions.map((def) => ({
358− toolId: `${namespace}.${def.toolPath}`,
359− sourceId: namespace,
360− binding: toBinding(def),
361− }));
362−
363− yield* ctx.transaction(
364− Effect.gen(function* () {
365− yield* ctx.storage.upsertSource(storedSource, storedOps);
366−
367− yield* ctx.core.sources.register({
368− id: namespace,
369− scope: config.scope,
370− kind: "openapi",
371− name: sourceName,
372− url: baseUrl || undefined,
373− canRemove: true,
374− canRefresh: false,
375− canEdit: true,
376− tools: definitions.map((def) => ({
377− name: def.toolPath,
378− description: descriptionFor(def),
379− inputSchema: normalizeOpenApiRefs(
380− Option.getOrUndefined(def.operation.inputSchema),
381− ),
382− outputSchema: normalizeOpenApiRefs(
383− Option.getOrUndefined(def.operation.outputSchema),
384− ),
385− })),
386− });
387−
388− if (Object.keys(hoistedDefs).length > 0) {
389− yield* ctx.core.definitions.register({
390− sourceId: namespace,
391− scope: config.scope,
392− definitions: hoistedDefs,
393− });
394− }
395− }),
396− );
397−
398− return { sourceId: namespace, toolCount: definitions.length };
466+ oauth2: config.oauth2,
467+ });
399468 });
400469
401470 const configFile = options?.configFile;
@@ -802,6 +871,15 @@ export const openApiPlugin = definePlugin( · 400 unmodified lines
802871 removeSource: ({ ctx, sourceId, scope }) =>
803872 ctx.storage.removeSource(sourceId, scope),
804873
874+ // Re-fetch the spec from its origin URL (captured at addSpec time)
875+ // and replay the same parse → extract → upsertSource → register
876+ // path used by addSpec. Sources without a stored URL surface a
877+ // typed `OpenApiParseError` — the executor only dispatches refresh
878+ // when `canRefresh: true`, so a raw-text source reaching here
879+ // means stale UI state, which is worth surfacing to the caller.
880+ refreshSource: ({ ctx, sourceId, scope }) =>
881+ refreshSourceInternal(ctx, sourceId, scope),
882+
805883 detect: ({ url }) =>
806884 Effect.gen(function* () {
807885 const trimmed = url.trim();
packages/plugins/openapi/src/sdk/store.ts+11−0
@@ -29,6 +29,11 @@ export const openapiSchema = defineSchema({ · 28 unmodified lines
2929 scope_id: { type: "string", required: true, index: true },
3030 name: { type: "string", required: true },
3131 spec: { type: "string", required: true },
32+ // Origin URL the spec was fetched from. Set when `addSpec` was
33+ // invoked with an http(s) URL; null when the caller passed raw
34+ // spec text. Drives `canRefresh` on the core source row and
35+ // is the address re-fetched on `refreshSource`.
36+ source_url: { type: "string", required: false },
3237 base_url: { type: "string", required: false },
3338 headers: { type: "json", required: false },
3439 oauth2: { type: "json", required: false },
@@ -62,6 +67,9 @@ export type OpenapiSchema = typeof openapiSchema; · 27 unmodified lines
6267
6368 export interface SourceConfig {
6469 readonly spec: string;
70+ /** Origin URL when the spec was fetched from http(s). Absent for
71+ * raw-text adds. Persisted so `refreshSource` can re-fetch. */
72+ readonly sourceUrl?: string;
6573 readonly baseUrl?: string;
6674 readonly namespace?: string;
6775 readonly headers?: Record<string, HeaderValue>;
@@ -92,6 +100,7 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( · 24 unmodified lines
92100 name: Schema.String,
93101 config: Schema.Struct({
94102 spec: Schema.String,
103+ sourceUrl: Schema.optional(Schema.String),
95104 baseUrl: Schema.optional(Schema.String),
96105 namespace: Schema.optional(Schema.String),
97106 headers: Schema.optional(
@@ -239,6 +248,7 @@ export const makeDefaultOpenapiStore = ({ · 141 unmodified lines
239248 name: row.name as string,
240249 config: {
241250 spec: row.spec as string,
251+ sourceUrl: (row.source_url as string | null | undefined) ?? undefined,
242252 baseUrl: (row.base_url as string | null | undefined) ?? undefined,
243253 headers,
244254 oauth2,
@@ -284,6 +294,7 @@ export const makeDefaultOpenapiStore = ({ · 39 unmodified lines
284294 scope_id: input.scope,
285295 name: input.name,
286296 spec: input.config.spec,
297+ source_url: input.config.sourceUrl ?? undefined,
287298 base_url: input.config.baseUrl ?? undefined,
288299 headers: (input.config.headers ?? {}) as unknown as Record<string, unknown>,
289300 oauth2: input.config.oauth2