|
|
@@ -0,0 +1,159 @@
|
|
|
+import { Account } from "@/account/account"
|
|
|
+import { Config } from "@/config"
|
|
|
+import { MCP } from "@/mcp"
|
|
|
+import { ToolRegistry } from "@/tool"
|
|
|
+import { Effect, Layer, Option, Schema } from "effect"
|
|
|
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
|
+import { Authorization } from "./auth"
|
|
|
+
|
|
|
+const ConsoleStateResponse = Schema.Struct({
|
|
|
+ consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
|
|
+ activeOrgName: Schema.optionalKey(Schema.String),
|
|
|
+ switchableOrgCount: Schema.Number,
|
|
|
+}).annotate({ identifier: "ConsoleState" })
|
|
|
+
|
|
|
+const ConsoleOrgOption = Schema.Struct({
|
|
|
+ accountID: Schema.String,
|
|
|
+ accountEmail: Schema.String,
|
|
|
+ accountUrl: Schema.String,
|
|
|
+ orgID: Schema.String,
|
|
|
+ orgName: Schema.String,
|
|
|
+ active: Schema.Boolean,
|
|
|
+}).annotate({ identifier: "ConsoleOrgOption" })
|
|
|
+
|
|
|
+const ConsoleOrgList = Schema.Struct({
|
|
|
+ orgs: Schema.Array(ConsoleOrgOption),
|
|
|
+}).annotate({ identifier: "ConsoleOrgList" })
|
|
|
+
|
|
|
+const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" })
|
|
|
+
|
|
|
+export const ExperimentalPaths = {
|
|
|
+ console: "/experimental/console",
|
|
|
+ consoleOrgs: "/experimental/console/orgs",
|
|
|
+ toolIDs: "/experimental/tool/ids",
|
|
|
+ resource: "/experimental/resource",
|
|
|
+} as const
|
|
|
+
|
|
|
+export const ExperimentalApi = HttpApi.make("experimental")
|
|
|
+ .add(
|
|
|
+ HttpApiGroup.make("experimental")
|
|
|
+ .add(
|
|
|
+ HttpApiEndpoint.get("console", ExperimentalPaths.console, {
|
|
|
+ success: ConsoleStateResponse,
|
|
|
+ }).annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ identifier: "experimental.console.get",
|
|
|
+ summary: "Get active Console provider metadata",
|
|
|
+ description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
|
|
|
+ success: ConsoleOrgList,
|
|
|
+ }).annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ identifier: "experimental.console.listOrgs",
|
|
|
+ summary: "List switchable Console orgs",
|
|
|
+ description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
|
|
+ success: ToolIDs,
|
|
|
+ }).annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ identifier: "tool.ids",
|
|
|
+ summary: "List tool IDs",
|
|
|
+ description:
|
|
|
+ "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
|
|
|
+ success: Schema.Record(Schema.String, MCP.Resource),
|
|
|
+ }).annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ identifier: "experimental.resource.list",
|
|
|
+ summary: "Get MCP resources",
|
|
|
+ description: "Get all available MCP resources from connected servers. Optionally filter by name.",
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ title: "experimental",
|
|
|
+ description: "Experimental HttpApi read-only routes.",
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .middleware(Authorization),
|
|
|
+ )
|
|
|
+ .annotateMerge(
|
|
|
+ OpenApi.annotations({
|
|
|
+ title: "opencode experimental HttpApi",
|
|
|
+ version: "0.0.1",
|
|
|
+ description: "Experimental HttpApi surface for selected instance routes.",
|
|
|
+ }),
|
|
|
+ )
|
|
|
+
|
|
|
+export const experimentalHandlers = Layer.unwrap(
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const account = yield* Account.Service
|
|
|
+ const config = yield* Config.Service
|
|
|
+ const mcp = yield* MCP.Service
|
|
|
+ const registry = yield* ToolRegistry.Service
|
|
|
+
|
|
|
+ const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
|
|
|
+ const [state, groups] = yield* Effect.all(
|
|
|
+ [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)],
|
|
|
+ {
|
|
|
+ concurrency: "unbounded",
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ consoleManagedProviders: state.consoleManagedProviders,
|
|
|
+ ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}),
|
|
|
+ switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () {
|
|
|
+ const [groups, active] = yield* Effect.all(
|
|
|
+ [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)],
|
|
|
+ {
|
|
|
+ concurrency: "unbounded",
|
|
|
+ },
|
|
|
+ )
|
|
|
+ const info = Option.getOrUndefined(active)
|
|
|
+ return {
|
|
|
+ orgs: groups.flatMap((group) =>
|
|
|
+ group.orgs.map((org) => ({
|
|
|
+ accountID: group.account.id,
|
|
|
+ accountEmail: group.account.email,
|
|
|
+ accountUrl: group.account.url,
|
|
|
+ orgID: org.id,
|
|
|
+ orgName: org.name,
|
|
|
+ active: !!info && info.id === group.account.id && info.active_org_id === org.id,
|
|
|
+ })),
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
|
|
|
+ return yield* registry.ids()
|
|
|
+ })
|
|
|
+
|
|
|
+ const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
|
|
|
+ return yield* mcp.resources()
|
|
|
+ })
|
|
|
+
|
|
|
+ return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) =>
|
|
|
+ handlers
|
|
|
+ .handle("console", getConsole)
|
|
|
+ .handle("consoleOrgs", listConsoleOrgs)
|
|
|
+ .handle("toolIDs", toolIDs)
|
|
|
+ .handle("resource", resource),
|
|
|
+ )
|
|
|
+ }),
|
|
|
+).pipe(
|
|
|
+ Layer.provide(Account.defaultLayer),
|
|
|
+ Layer.provide(Config.defaultLayer),
|
|
|
+ Layer.provide(MCP.defaultLayer),
|
|
|
+ Layer.provide(ToolRegistry.defaultLayer),
|
|
|
+)
|