2026-04-17 22:31:02 +00:00
|
|
|
import { createHash } from "node:crypto"
|
2026-01-16 21:21:13 +00:00
|
|
|
import { Hono } from "hono"
|
|
|
|
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
|
|
|
|
import z from "zod"
|
2026-03-12 14:48:17 +00:00
|
|
|
import { ProviderID, ModelID } from "../../provider/schema"
|
2026-04-16 03:56:54 +00:00
|
|
|
import { ToolRegistry } from "../../tool"
|
2026-01-16 21:21:13 +00:00
|
|
|
import { Worktree } from "../../worktree"
|
|
|
|
|
import { Instance } from "../../project/instance"
|
2026-04-16 03:28:46 +00:00
|
|
|
import { Project } from "../../project"
|
2026-01-16 21:21:13 +00:00
|
|
|
import { MCP } from "../../mcp"
|
2026-02-20 13:45:12 +00:00
|
|
|
import { Session } from "../../session"
|
2026-04-16 01:26:24 +00:00
|
|
|
import { Config } from "../../config"
|
2026-04-04 23:05:45 +00:00
|
|
|
import { ConsoleState } from "../../config/console-state"
|
|
|
|
|
import { Account, AccountID, OrgID } from "../../account"
|
2026-04-11 18:16:36 +00:00
|
|
|
import { AppRuntime } from "../../effect/app-runtime"
|
2026-01-16 21:21:13 +00:00
|
|
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
|
|
|
|
import { errors } from "../error"
|
|
|
|
|
import { lazy } from "../../util/lazy"
|
2026-04-11 18:16:36 +00:00
|
|
|
import { Effect, Option } from "effect"
|
2026-02-27 20:36:39 +00:00
|
|
|
import { WorkspaceRoutes } from "./workspace"
|
2026-03-28 17:28:24 +00:00
|
|
|
import { PushRelay } from "../push-relay"
|
2026-04-02 13:00:08 +00:00
|
|
|
import * as QRCode from "qrcode"
|
2026-04-07 23:48:12 +00:00
|
|
|
import { Agent } from "@/agent/agent"
|
2026-04-02 13:00:08 +00:00
|
|
|
|
|
|
|
|
const PushPairPayload = z
|
|
|
|
|
.object({
|
|
|
|
|
relaySecret: z.string(),
|
|
|
|
|
hosts: z.array(z.string()),
|
|
|
|
|
})
|
|
|
|
|
.meta({ ref: "PushPairPayload" })
|
|
|
|
|
|
|
|
|
|
const PushPairResult = z
|
|
|
|
|
.discriminatedUnion("enabled", [
|
|
|
|
|
z.object({
|
|
|
|
|
enabled: z.literal(false),
|
|
|
|
|
}),
|
|
|
|
|
z.object({
|
|
|
|
|
enabled: z.literal(true),
|
|
|
|
|
hosts: z.array(z.string()),
|
2026-04-17 22:31:02 +00:00
|
|
|
relayURL: z.string(),
|
|
|
|
|
serverID: z.string().optional(),
|
|
|
|
|
relaySecretHash: z.string(),
|
2026-04-08 21:01:32 +00:00
|
|
|
link: z.string(),
|
2026-04-02 13:00:08 +00:00
|
|
|
qr: z.string(),
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
.meta({ ref: "PushPairResult" })
|
|
|
|
|
|
|
|
|
|
const pushPairQROptions = {
|
|
|
|
|
errorCorrectionLevel: "M" as const,
|
|
|
|
|
margin: 1,
|
|
|
|
|
width: 256,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 21:17:36 +00:00
|
|
|
function pushPairLink(input: { relaySecret: string; hosts: string[] }) {
|
|
|
|
|
const payload: z.infer<typeof PushPairPayload> = {
|
|
|
|
|
relaySecret: input.relaySecret,
|
|
|
|
|
hosts: input.hosts,
|
|
|
|
|
}
|
2026-04-02 13:00:08 +00:00
|
|
|
return `mobilevoice:///?pair=${encodeURIComponent(JSON.stringify(payload))}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 21:17:36 +00:00
|
|
|
async function pushPairQRCode(input: { relaySecret: string; hosts: string[] }) {
|
|
|
|
|
return QRCode.toDataURL(pushPairLink(input), pushPairQROptions)
|
2026-04-02 13:00:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 23:05:45 +00:00
|
|
|
const ConsoleOrgOption = z.object({
|
|
|
|
|
accountID: z.string(),
|
|
|
|
|
accountEmail: z.string(),
|
|
|
|
|
accountUrl: z.string(),
|
|
|
|
|
orgID: z.string(),
|
|
|
|
|
orgName: z.string(),
|
|
|
|
|
active: z.boolean(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const ConsoleOrgList = z.object({
|
|
|
|
|
orgs: z.array(ConsoleOrgOption),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const ConsoleSwitchBody = z.object({
|
|
|
|
|
accountID: z.string(),
|
|
|
|
|
orgID: z.string(),
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-16 21:21:54 +00:00
|
|
|
export const ExperimentalRoutes = lazy(() =>
|
|
|
|
|
new Hono()
|
2026-04-04 23:05:45 +00:00
|
|
|
.get(
|
|
|
|
|
"/console",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Get active Console provider metadata",
|
|
|
|
|
description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
|
|
|
|
|
operationId: "experimental.console.get",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Active Console provider metadata",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(ConsoleState),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
async (c) => {
|
2026-04-11 18:16:36 +00:00
|
|
|
const result = await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const config = yield* Config.Service
|
|
|
|
|
const account = yield* Account.Service
|
2026-04-11 18:18:40 +00:00
|
|
|
const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], {
|
|
|
|
|
concurrency: "unbounded",
|
|
|
|
|
})
|
2026-04-11 18:16:36 +00:00
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
return c.json(result)
|
2026-04-04 23:05:45 +00:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.get(
|
|
|
|
|
"/console/orgs",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "List switchable Console orgs",
|
|
|
|
|
description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
|
|
|
|
operationId: "experimental.console.listOrgs",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Switchable Console orgs",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(ConsoleOrgList),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
async (c) => {
|
2026-04-11 18:16:36 +00:00
|
|
|
const orgs = await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const account = yield* Account.Service
|
2026-04-11 18:18:40 +00:00
|
|
|
const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], {
|
|
|
|
|
concurrency: "unbounded",
|
|
|
|
|
})
|
2026-04-11 18:16:36 +00:00
|
|
|
const info = Option.getOrUndefined(active)
|
|
|
|
|
return 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,
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
}),
|
2026-04-04 23:05:45 +00:00
|
|
|
)
|
|
|
|
|
return c.json({ orgs })
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.post(
|
|
|
|
|
"/console/switch",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Switch active Console org",
|
|
|
|
|
description: "Persist a new active Console account/org selection for the current local OpenCode state.",
|
|
|
|
|
operationId: "experimental.console.switchOrg",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Switch success",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.boolean()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
validator("json", ConsoleSwitchBody),
|
|
|
|
|
async (c) => {
|
|
|
|
|
const body = c.req.valid("json")
|
2026-04-11 18:16:36 +00:00
|
|
|
await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const account = yield* Account.Service
|
|
|
|
|
yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID)))
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-04-04 23:05:45 +00:00
|
|
|
return c.json(true)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-01-16 21:21:54 +00:00
|
|
|
.get(
|
|
|
|
|
"/tool/ids",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "List tool IDs",
|
|
|
|
|
description:
|
|
|
|
|
"Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
|
|
|
|
|
operationId: "tool.ids",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Tool IDs",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
|
|
|
|
|
},
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
...errors(400),
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
}),
|
|
|
|
|
async (c) => {
|
2026-04-13 15:09:32 +00:00
|
|
|
const ids = await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
|
return yield* registry.ids()
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
return c.json(ids)
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
)
|
|
|
|
|
.get(
|
|
|
|
|
"/tool",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "List tools",
|
|
|
|
|
description:
|
|
|
|
|
"Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
|
|
|
|
|
operationId: "tool.list",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Tools",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(
|
|
|
|
|
z
|
|
|
|
|
.array(
|
|
|
|
|
z
|
|
|
|
|
.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
description: z.string(),
|
|
|
|
|
parameters: z.any(),
|
|
|
|
|
})
|
|
|
|
|
.meta({ ref: "ToolListItem" }),
|
|
|
|
|
)
|
|
|
|
|
.meta({ ref: "ToolList" }),
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
...errors(400),
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
}),
|
2026-01-16 21:21:54 +00:00
|
|
|
validator(
|
|
|
|
|
"query",
|
|
|
|
|
z.object({
|
|
|
|
|
provider: z.string(),
|
|
|
|
|
model: z.string(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
async (c) => {
|
2026-01-18 06:35:09 +00:00
|
|
|
const { provider, model } = c.req.valid("query")
|
2026-04-13 15:09:32 +00:00
|
|
|
const tools = await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const agents = yield* Agent.Service
|
|
|
|
|
const registry = yield* ToolRegistry.Service
|
|
|
|
|
return yield* registry.tools({
|
|
|
|
|
providerID: ProviderID.make(provider),
|
|
|
|
|
modelID: ModelID.make(model),
|
|
|
|
|
agent: yield* agents.get(yield* agents.defaultAgent()),
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-01-16 21:21:54 +00:00
|
|
|
return c.json(
|
|
|
|
|
tools.map((t) => ({
|
|
|
|
|
id: t.id,
|
|
|
|
|
description: t.description,
|
|
|
|
|
// Handle both Zod schemas and plain JSON schemas
|
|
|
|
|
parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-04 02:35:38 +00:00
|
|
|
.route("/workspace", WorkspaceRoutes())
|
2026-01-16 21:21:54 +00:00
|
|
|
.post(
|
|
|
|
|
"/worktree",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Create worktree",
|
2026-01-22 13:41:20 +00:00
|
|
|
description: "Create a new git worktree for the current project and run any configured startup scripts.",
|
2026-01-16 21:21:54 +00:00
|
|
|
operationId: "worktree.create",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Worktree created",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(Worktree.Info),
|
|
|
|
|
},
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
...errors(400),
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
}),
|
2026-03-25 00:26:21 +00:00
|
|
|
validator("json", Worktree.CreateInput.optional()),
|
2026-01-16 21:21:54 +00:00
|
|
|
async (c) => {
|
|
|
|
|
const body = c.req.valid("json")
|
2026-04-14 01:23:15 +00:00
|
|
|
const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.create(body)))
|
2026-01-16 21:21:54 +00:00
|
|
|
return c.json(worktree)
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
)
|
|
|
|
|
.get(
|
|
|
|
|
"/worktree",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "List worktrees",
|
|
|
|
|
description: "List all sandbox worktrees for the current project.",
|
|
|
|
|
operationId: "worktree.list",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "List of worktree directories",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.array(z.string())),
|
|
|
|
|
},
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
}),
|
|
|
|
|
async (c) => {
|
2026-04-14 16:49:20 +00:00
|
|
|
const sandboxes = await AppRuntime.runPromise(Project.Service.use((svc) => svc.sandboxes(Instance.project.id)))
|
2026-01-16 21:21:54 +00:00
|
|
|
return c.json(sandboxes)
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
)
|
2026-01-15 18:48:54 +00:00
|
|
|
.delete(
|
|
|
|
|
"/worktree",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Remove worktree",
|
|
|
|
|
description: "Remove a git worktree and delete its branch.",
|
|
|
|
|
operationId: "worktree.remove",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Worktree removed",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.boolean()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
...errors(400),
|
|
|
|
|
},
|
|
|
|
|
}),
|
2026-03-25 00:26:21 +00:00
|
|
|
validator("json", Worktree.RemoveInput),
|
2026-01-15 18:48:54 +00:00
|
|
|
async (c) => {
|
|
|
|
|
const body = c.req.valid("json")
|
2026-04-14 01:23:15 +00:00
|
|
|
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove(body)))
|
2026-04-14 16:49:20 +00:00
|
|
|
await AppRuntime.runPromise(
|
|
|
|
|
Project.Service.use((svc) => svc.removeSandbox(Instance.project.id, body.directory)),
|
|
|
|
|
)
|
2026-01-15 18:48:54 +00:00
|
|
|
return c.json(true)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-01-15 19:33:21 +00:00
|
|
|
.post(
|
|
|
|
|
"/worktree/reset",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Reset worktree",
|
|
|
|
|
description: "Reset a worktree branch to the primary default branch.",
|
|
|
|
|
operationId: "worktree.reset",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Worktree reset",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.boolean()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
...errors(400),
|
|
|
|
|
},
|
|
|
|
|
}),
|
2026-03-25 00:26:21 +00:00
|
|
|
validator("json", Worktree.ResetInput),
|
2026-01-15 19:33:21 +00:00
|
|
|
async (c) => {
|
|
|
|
|
const body = c.req.valid("json")
|
2026-04-14 01:23:15 +00:00
|
|
|
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.reset(body)))
|
2026-01-15 19:33:21 +00:00
|
|
|
return c.json(true)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-02-20 13:45:12 +00:00
|
|
|
.get(
|
|
|
|
|
"/session",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "List sessions",
|
|
|
|
|
description:
|
|
|
|
|
"Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.",
|
|
|
|
|
operationId: "experimental.session.list",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "List of sessions",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(Session.GlobalInfo.array()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
validator(
|
|
|
|
|
"query",
|
|
|
|
|
z.object({
|
|
|
|
|
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
|
|
|
|
|
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
|
|
|
|
start: z.coerce
|
|
|
|
|
.number()
|
|
|
|
|
.optional()
|
|
|
|
|
.meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
|
|
|
|
|
cursor: z.coerce
|
|
|
|
|
.number()
|
|
|
|
|
.optional()
|
|
|
|
|
.meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
|
|
|
|
|
search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
|
|
|
|
limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
|
|
|
|
|
archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
async (c) => {
|
|
|
|
|
const query = c.req.valid("query")
|
|
|
|
|
const limit = query.limit ?? 100
|
|
|
|
|
const sessions: Session.GlobalInfo[] = []
|
|
|
|
|
for await (const session of Session.listGlobal({
|
|
|
|
|
directory: query.directory,
|
|
|
|
|
roots: query.roots,
|
|
|
|
|
start: query.start,
|
|
|
|
|
cursor: query.cursor,
|
|
|
|
|
search: query.search,
|
|
|
|
|
limit: limit + 1,
|
|
|
|
|
archived: query.archived,
|
|
|
|
|
})) {
|
|
|
|
|
sessions.push(session)
|
|
|
|
|
}
|
|
|
|
|
const hasMore = sessions.length > limit
|
|
|
|
|
const list = hasMore ? sessions.slice(0, limit) : sessions
|
|
|
|
|
if (hasMore && list.length > 0) {
|
|
|
|
|
c.header("x-next-cursor", String(list[list.length - 1].time.updated))
|
|
|
|
|
}
|
|
|
|
|
return c.json(list)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-01-16 21:21:54 +00:00
|
|
|
.get(
|
|
|
|
|
"/resource",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Get MCP resources",
|
|
|
|
|
description: "Get all available MCP resources from connected servers. Optionally filter by name.",
|
|
|
|
|
operationId: "experimental.resource.list",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "MCP resources",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(z.record(z.string(), MCP.Resource)),
|
|
|
|
|
},
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
}),
|
|
|
|
|
async (c) => {
|
2026-04-13 17:45:34 +00:00
|
|
|
return c.json(
|
|
|
|
|
await AppRuntime.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const mcp = yield* MCP.Service
|
|
|
|
|
return yield* mcp.resources()
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-01-16 21:21:13 +00:00
|
|
|
},
|
2026-03-28 17:28:24 +00:00
|
|
|
)
|
2026-04-02 13:00:08 +00:00
|
|
|
.get(
|
|
|
|
|
"/push/pair",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Get push relay pairing QR",
|
|
|
|
|
description: "Get the active push relay pairing payload and QR code for mobile setup.",
|
|
|
|
|
operationId: "experimental.push.pair",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Push relay pairing info",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(PushPairResult),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
async (c) => {
|
|
|
|
|
const pair = PushRelay.pair()
|
|
|
|
|
if (!pair) {
|
|
|
|
|
return c.json({
|
|
|
|
|
enabled: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 21:01:32 +00:00
|
|
|
const link = pushPairLink(pair)
|
2026-04-02 13:00:08 +00:00
|
|
|
const qr = await pushPairQRCode(pair)
|
2026-04-17 22:31:02 +00:00
|
|
|
const relaySecretHash = pair.relaySecret
|
|
|
|
|
? `${createHash("sha256").update(pair.relaySecret).digest("hex").slice(0, 12)}...`
|
|
|
|
|
: "none"
|
2026-04-02 13:00:08 +00:00
|
|
|
|
|
|
|
|
return c.json({
|
|
|
|
|
enabled: true,
|
|
|
|
|
hosts: pair.hosts,
|
2026-04-17 22:31:02 +00:00
|
|
|
relayURL: pair.relayURL,
|
|
|
|
|
serverID: pair.serverID,
|
|
|
|
|
relaySecretHash,
|
2026-04-08 21:01:32 +00:00
|
|
|
link,
|
2026-04-02 13:00:08 +00:00
|
|
|
qr,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-28 17:28:24 +00:00
|
|
|
.get(
|
|
|
|
|
"/push",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Get push relay status",
|
|
|
|
|
description: "Get experimental push relay runtime status for this server.",
|
|
|
|
|
operationId: "experimental.push.status",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Push relay status",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(
|
|
|
|
|
z.object({
|
|
|
|
|
enabled: z.boolean(),
|
|
|
|
|
relaySecretSet: z.boolean(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
async (c) => {
|
|
|
|
|
return c.json(PushRelay.status())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.post(
|
|
|
|
|
"/push/test",
|
|
|
|
|
describeRoute({
|
|
|
|
|
summary: "Send test push event",
|
|
|
|
|
description: "Send a test push event through the experimental APN relay integration.",
|
|
|
|
|
operationId: "experimental.push.test",
|
|
|
|
|
responses: {
|
|
|
|
|
200: {
|
|
|
|
|
description: "Test event accepted",
|
|
|
|
|
content: {
|
|
|
|
|
"application/json": {
|
|
|
|
|
schema: resolver(
|
|
|
|
|
z.object({
|
|
|
|
|
ok: z.boolean(),
|
|
|
|
|
enabled: z.boolean(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
...errors(400),
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
validator(
|
|
|
|
|
"json",
|
|
|
|
|
z.object({
|
|
|
|
|
secret: z.string(),
|
|
|
|
|
sessionID: z.string().optional(),
|
|
|
|
|
eventType: z.enum(["complete", "permission", "error"]).optional(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
async (c) => {
|
|
|
|
|
const body = c.req.valid("json")
|
|
|
|
|
const status = PushRelay.status()
|
|
|
|
|
if (!status.enabled) {
|
|
|
|
|
return c.json(
|
|
|
|
|
{
|
|
|
|
|
data: { enabled: false },
|
|
|
|
|
errors: [{ message: "Push relay is not enabled" }],
|
|
|
|
|
success: false,
|
|
|
|
|
},
|
|
|
|
|
400,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!PushRelay.auth(body.secret)) {
|
|
|
|
|
return c.json(
|
|
|
|
|
{
|
|
|
|
|
data: { enabled: true },
|
|
|
|
|
errors: [{ message: "Invalid push relay secret" }],
|
|
|
|
|
success: false,
|
|
|
|
|
},
|
|
|
|
|
400,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ok = PushRelay.test({
|
|
|
|
|
type: body.eventType ?? "permission",
|
|
|
|
|
sessionID: body.sessionID ?? `test-${Date.now()}`,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return c.json({
|
|
|
|
|
ok,
|
|
|
|
|
enabled: true,
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-01-16 21:21:54 +00:00
|
|
|
),
|
2026-01-16 21:21:13 +00:00
|
|
|
)
|