diff --git a/cc-haha/desktop/src/api/heicodeResources.ts b/cc-haha/desktop/src/api/heicodeResources.ts new file mode 100644 index 0000000..de052e6 --- /dev/null +++ b/cc-haha/desktop/src/api/heicodeResources.ts @@ -0,0 +1,97 @@ +// desktop/src/api/heicodeResources.ts +// +// API client for the local server's /api/heicode-resources/* proxy. +// The local server forwards each call to mcp-server (apimtaiji.azure-api.net) +// using the JWT stored in the active provider's mcpAuth, with transparent +// refresh on the 24h boundary. +// +// Server contract: cc-haha/src/server/api/heicode-resources.ts +// mcp-server contract: Heicode-接口契约文档.md §2 + §3 + +import { api } from './client' + +export type ResourceType = + | 'git' + | 'sk' + | 'project_doc' + | 'cloud_account' + | 'cloud_resource' + +export type ResourceStatus = 'pending' | 'active' | 'disabled' | 'revoked' + +export type ResourceBinding = { + id: string + user_id: string + type: ResourceType + name: string + external_ref?: string + metadata: Record + permission_scope: string[] + constraints: Record + secret_ref?: string + status: ResourceStatus + created_by: string + updated_by: string + created_at: string + updated_at: string +} + +export type ResourceBindingsList = { + items: ResourceBinding[] + total: number + offset: number + limit: number +} + +export type GrantStatus = 'active' | 'suspended' | 'revoked' | 'expired' + +export type ResourceGrant = { + id: string + user_id: string + resource_id: string + binding_scope: string + role?: string + agent_id?: string | null + allowed_actions: string[] + constraints: Record + status: GrantStatus + expires_at?: string | null + created_by: string + revoked_by?: string | null + created_at: string + revoked_at?: string | null +} + +export type ResourceGrantsList = { + items: ResourceGrant[] + total: number + offset: number + limit: number +} + +export type ApiEnvelope = { + success: boolean + data?: T + message?: string | null +} + +const BASE = '/api/heicode-resources' + +export const heicodeResourcesApi = { + listBindings(opts?: { type?: ResourceType; status?: ResourceStatus }) { + const params = new URLSearchParams() + if (opts?.type) params.set('type', opts.type) + if (opts?.status) params.set('status', opts.status) + const qs = params.toString() + return api.get>( + `${BASE}${qs ? `?${qs}` : ''}`, + ) + }, + + getBinding(id: string) { + return api.get>(`${BASE}/${encodeURIComponent(id)}`) + }, + + // Slice 4 will add createBinding / updateBinding / deleteBinding / + // listGrants / createGrant / revokeGrant. +} diff --git a/cc-haha/desktop/src/i18n/locales/en.ts b/cc-haha/desktop/src/i18n/locales/en.ts index 8568d16..6aba2da 100644 --- a/cc-haha/desktop/src/i18n/locales/en.ts +++ b/cc-haha/desktop/src/i18n/locales/en.ts @@ -10,6 +10,7 @@ export const en = { 'common.rename': 'Rename', 'common.retry': 'Retry', 'common.loading': 'Loading...', + 'common.dismiss': 'Dismiss', 'common.select': 'Select', 'common.enable': 'Enable', 'common.disable': 'Disable', @@ -959,9 +960,18 @@ export const en = { // ─── Resource Bindings (mcp-server P1 nine endpoints) ──── 'resources.title': 'Resources', 'resources.subtitle': 'Bind Git repos / SK / project docs / cloud accounts / cloud resources, then grant them to sub-agents.', - 'resources.empty.title': 'Not wired up yet', - 'resources.empty.body': 'This page integrates the Heicode Manager P1 resource model APIs (mcp-server has 9 endpoints live). Create / list / revoke bindings and grants are coming soon.', - 'resources.empty.comingSoon': 'Coming soon — Manager service-credential auth and grant UI under development', + 'resources.refresh': 'Refresh', + 'resources.refreshing': 'Refreshing…', + 'resources.empty.title': 'No resource bindings yet', + 'resources.empty.body': 'Create Git repo / SK / project doc / cloud account / cloud resource bindings under your Heicode Manager account to manage and grant them here.', + 'resources.empty.comingSoon': 'Create / delete UI ships in the next slice; until then create them via Heicode Web console.', + 'resources.error.title': 'Failed to load', + 'resources.col.name': 'Name', + 'resources.col.type': 'Type', + 'resources.col.externalRef': 'External ref', + 'resources.col.status': 'Status', + 'resources.col.scope': 'Permission scope', + 'resources.footer.cruComingSoon': 'This version only lists existing bindings. Create / edit / delete / grants UI ships in the next slice.', // ─── HeiCode Login ────────────────────────────────────── 'login.subtitle': 'Sign in to start using Heicode', diff --git a/cc-haha/desktop/src/i18n/locales/zh.ts b/cc-haha/desktop/src/i18n/locales/zh.ts index 7f711c2..7b76486 100644 --- a/cc-haha/desktop/src/i18n/locales/zh.ts +++ b/cc-haha/desktop/src/i18n/locales/zh.ts @@ -12,6 +12,7 @@ export const zh: Record = { 'common.rename': '重命名', 'common.retry': '重试', 'common.loading': '加载中...', + 'common.dismiss': '关闭', 'common.select': '选择', 'common.enable': '启用', 'common.disable': '禁用', @@ -961,9 +962,18 @@ export const zh: Record = { // ─── 资源绑定(接 mcp-server P1 9 接口)────────────── 'resources.title': '资源绑定', 'resources.subtitle': '绑定 Git 仓库 / SK / 项目文档 / 云账号 / 云资源,并把它们授给子 Agent 使用。', - 'resources.empty.title': '尚未实装', - 'resources.empty.body': '本页对接 Heicode Manager 的 P1 资源模型接口(mcp-server 已上线 9 个端点)。后续将支持创建 / 列表 / 撤销绑定与授权。', - 'resources.empty.comingSoon': '即将上线 — Manager 服务凭据接入与授权 UI 开发中', + 'resources.refresh': '刷新', + 'resources.refreshing': '刷新中…', + 'resources.empty.title': '还没有任何资源绑定', + 'resources.empty.body': '在 Heicode Manager 平台账号下创建 Git 仓库、SK、项目文档、云账号或云资源的绑定,便可在此处统一管理与授权。', + 'resources.empty.comingSoon': '客户端创建/删除入口在下一版本上线;当前可在 Heicode Web 控制台创建。', + 'resources.error.title': '加载失败', + 'resources.col.name': '名称', + 'resources.col.type': '类型', + 'resources.col.externalRef': '外部引用', + 'resources.col.status': '状态', + 'resources.col.scope': '权限范围', + 'resources.footer.cruComingSoon': '当前版本仅展示绑定列表。新建 / 编辑 / 删除 / 授权管理将在下一版本提供。', // ─── HeiCode 登录 ────────────────────────────────────── 'login.subtitle': '登录以开始使用 Heicode', diff --git a/cc-haha/desktop/src/pages/ResourceBindings.tsx b/cc-haha/desktop/src/pages/ResourceBindings.tsx index 207bec2..b80a328 100644 --- a/cc-haha/desktop/src/pages/ResourceBindings.tsx +++ b/cc-haha/desktop/src/pages/ResourceBindings.tsx @@ -1,52 +1,178 @@ // desktop/src/pages/ResourceBindings.tsx // -// 资源绑定页面 — 接 mcp-server 的 P1 9 接口(已上线): -// GET /api/resources 列出当前用户所有 ResourceBinding -// POST /api/resources 新建 binding(git/sk/project_doc/cloud_account/cloud_resource) -// GET /api/resources/{id} 单个 binding 详情 -// PUT /api/resources/{id} 更新 binding -// DELETE /api/resources/{id} 软删除(status=revoked) -// POST /api/resource-grants 把 binding 授给 role/agent -// GET /api/resource-grants 列出当前用户所有 grant -// GET /api/resource-grants/{id} 单个 grant -// DELETE /api/resource-grants/{id} 撤销 -// -// 当前为切片 2 的 shell —— 显示标题 + 占位说明,让用户能看到入口。 -// 切片 3+4 接入实际 CRUD。 +// 资源绑定页面。Slice 3:列出当前用户的所有 ResourceBinding(接 mcp-server +// /api/resources,通过本地 server /api/heicode-resources 代理)。 +// Slice 4 再加 Create/Delete + Grants。 +import { useEffect } from 'react' import { useTranslation } from '../i18n' +import { useResourceStore } from '../stores/resourceStore' +import type { ResourceBinding, ResourceStatus } from '../api/heicodeResources' + +const TYPE_LABELS: Record = { + git: 'Git', + sk: 'SK', + project_doc: '项目文档', + cloud_account: '云账号', + cloud_resource: '云资源', +} + +const STATUS_TONE: Record = { + active: 'text-[var(--color-success)] border-[var(--color-success)]/30 bg-[var(--color-success)]/10', + pending: 'text-[var(--color-warning)] border-[var(--color-warning)]/30 bg-[var(--color-warning)]/10', + disabled: 'text-[var(--color-text-tertiary)] border-[var(--color-border)] bg-[var(--color-surface-container-low)]', + revoked: 'text-[var(--color-error)] border-[var(--color-error)]/30 bg-[var(--color-error)]/10', +} export function ResourceBindings() { const t = useTranslation() + const { bindings, isLoading, hasFetched, error, fetchBindings, clearError } = + useResourceStore() + + useEffect(() => { + if (!hasFetched) { + void fetchBindings() + } + }, [hasFetched, fetchBindings]) + + const showEmpty = hasFetched && !isLoading && !error && bindings.length === 0 + return (
-
-
-

+
+
+

+ {t('resources.title')} +

+

+ {t('resources.subtitle')} +

+
+

-

- {t('resources.subtitle')} -

+ {isLoading ? t('resources.refreshing') : t('resources.refresh')} +
-
-
- link + {error ? ( +
+
+
+
{t('resources.error.title')}
+
{error}
+
+ +
-

- {t('resources.empty.title')} -

-

- {t('resources.empty.body')} -

-

- {t('resources.empty.comingSoon')} -

-
+ ) : null} + + {!hasFetched && isLoading ? ( +
+ {t('common.loading')} +
+ ) : null} + + {showEmpty ? ( +
+
+ link_off +
+

+ {t('resources.empty.title')} +

+

+ {t('resources.empty.body')} +

+

+ {t('resources.empty.comingSoon')} +

+
+ ) : null} + + {bindings.length > 0 ? ( +
+ + + + + + + + + + + + {bindings.map((b) => ( + + + + + + + + ))} + +
{t('resources.col.name')}{t('resources.col.type')}{t('resources.col.externalRef')}{t('resources.col.status')}{t('resources.col.scope')}
+
{b.name}
+
+ {b.id.slice(0, 8)}… +
+
+ + {TYPE_LABELS[b.type] ?? b.type} + + + {b.external_ref ?? '—'} + + + {b.status} + + + {b.permission_scope.length > 0 ? ( +
+ {b.permission_scope.slice(0, 3).map((s) => ( + + {s} + + ))} + {b.permission_scope.length > 3 ? ( + + +{b.permission_scope.length - 3} + + ) : null} +
+ ) : ( + '—' + )} +
+
+ ) : null} + +
+ {t('resources.footer.cruComingSoon')} +
) diff --git a/cc-haha/desktop/src/stores/resourceStore.ts b/cc-haha/desktop/src/stores/resourceStore.ts new file mode 100644 index 0000000..aa8dfd2 --- /dev/null +++ b/cc-haha/desktop/src/stores/resourceStore.ts @@ -0,0 +1,51 @@ +// desktop/src/stores/resourceStore.ts +// +// Zustand store for ResourceBinding / ResourceGrant lists. Talks to the local +// /api/heicode-resources/* proxy (which forwards to mcp-server with refresh). +// +// Slice 3 scope: list bindings only. Slice 4 adds create/delete/grants. + +import { create } from 'zustand' +import { + heicodeResourcesApi, + type ResourceBinding, +} from '../api/heicodeResources' + +type ResourceState = { + bindings: ResourceBinding[] + isLoading: boolean + hasFetched: boolean + error: string | null + + fetchBindings: () => Promise + clearError: () => void +} + +export const useResourceStore = create((set) => ({ + bindings: [], + isLoading: false, + hasFetched: false, + error: null, + + fetchBindings: async () => { + set({ isLoading: true, error: null }) + try { + const res = await heicodeResourcesApi.listBindings() + const items = res.success ? (res.data?.items ?? []) : [] + set({ + bindings: items, + isLoading: false, + hasFetched: true, + error: res.success ? null : (res.message || '加载资源绑定失败'), + }) + } catch (err) { + set({ + isLoading: false, + hasFetched: true, + error: err instanceof Error ? err.message : String(err), + }) + } + }, + + clearError: () => set({ error: null }), +})) diff --git a/cc-haha/src/server/api/heicode-auth.ts b/cc-haha/src/server/api/heicode-auth.ts index b03555f..ee3fcb1 100644 --- a/cc-haha/src/server/api/heicode-auth.ts +++ b/cc-haha/src/server/api/heicode-auth.ts @@ -268,6 +268,28 @@ async function handleLoginWithCredentials(req: Request): Promise { if (!accessToken || !refreshToken) { throw ApiError.badRequest('平台登录响应缺少 token 字段') } + const mcpUserId = mgrJson.data?.user?.id + const mcpChannelId = mgrJson.data?.user?.channelId + + // Decode JWT exp claims (no signature verification — we already trust the + // platform we just authenticated against). Used downstream to know when to + // refresh the access token before each /api/resources call. + const accessExp = decodeJwtExpMs(accessToken) + const refreshExp = decodeJwtExpMs(refreshToken) + // Defaults if exp missing: assume documented TTLs (24h access / 7d refresh). + const now = Date.now() + const accessExpiresAt = accessExp ?? now + 24 * 3600 * 1000 + const refreshExpiresAt = refreshExp ?? now + 7 * 24 * 3600 * 1000 + + const mcpAuth = { + accessToken, + refreshToken, + accessExpiresAt, + refreshExpiresAt, + managerLoginUrl: managerBase, + ...(mcpUserId !== undefined && { userId: mcpUserId }), + ...(mcpChannelId !== undefined && { channelId: mcpChannelId }), + } // ── Step 2: POST /api/user/session/from-agnet ───────── const faRes = await fetch(`${base}/api/user/session/from-agnet`, { @@ -348,7 +370,29 @@ async function handleLoginWithCredentials(req: Request): Promise { } // ── Step 4: Save + activate via the existing pipeline ───────── - return await loginAndActivate(providerId, token, displayName) + return await loginAndActivate(providerId, token, displayName, mcpAuth) +} + +/** + * Decode the `exp` claim of a JWT (no signature verification — we already + * trust the issuer because we just successfully authenticated against it). + * Returns Unix epoch milliseconds, or null if the token is malformed. + */ +function decodeJwtExpMs(jwt: string): number | null { + try { + const parts = jwt.split('.') + if (parts.length !== 3) return null + const payloadB64 = parts[1]! + // base64url → base64 + const base64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4) + const json = Buffer.from(padded, 'base64').toString('utf-8') + const claims = JSON.parse(json) as { exp?: number } + if (typeof claims.exp !== 'number') return null + return claims.exp * 1000 // JWT exp is seconds; we want ms + } catch { + return null + } } // ─── OAuth scaffold (TODO: 平台支持后实装) ────────────────────── @@ -447,6 +491,15 @@ async function loginAndActivate( providerId: SupportedLoginProviderId, apiKey: string, displayName?: string, + mcpAuth?: { + accessToken: string + refreshToken: string + accessExpiresAt: number + refreshExpiresAt: number + managerLoginUrl: string + userId?: string + channelId?: string + }, ): Promise { if (!isSupportedLoginProvider(providerId)) { throw ApiError.badRequest(`Unsupported provider: ${providerId}`) @@ -496,6 +549,7 @@ async function loginAndActivate( baseUrl: preset.baseUrl, apiFormat: preset.apiFormat, models, + ...(mcpAuth && { mcpAuth }), }) } else { saved = await providerService.addProvider({ @@ -505,6 +559,7 @@ async function loginAndActivate( baseUrl: preset.baseUrl, apiFormat: preset.apiFormat, models, + ...(mcpAuth && { mcpAuth }), }) } await providerService.activateProvider(saved.id) diff --git a/cc-haha/src/server/api/heicode-resources.ts b/cc-haha/src/server/api/heicode-resources.ts new file mode 100644 index 0000000..5752905 --- /dev/null +++ b/cc-haha/src/server/api/heicode-resources.ts @@ -0,0 +1,193 @@ +/** + * Heicode Resources Proxy — exposes mcp-server's P1 ResourceBinding / + * ResourceGrant endpoints to the desktop client through the local server. + * + * Why proxy? + * - Desktop UI shouldn't deal with cross-origin / token refresh / Manager + * base URL switching. The local Bun server already holds the tokens + * (in providers.json under `mcpAuth`) so it's the natural place to + * attach the Authorization header and refresh transparently. + * + * Routes (mirror mcp-server contract `Heicode-接口契约文档.md` §2 + §3): + * + * GET /api/heicode-resources → /api/resources + * POST /api/heicode-resources → /api/resources + * GET /api/heicode-resources/:id → /api/resources/:id + * PUT /api/heicode-resources/:id → /api/resources/:id + * DELETE /api/heicode-resources/:id → /api/resources/:id + * + * GET /api/heicode-resources/grants → /api/resource-grants + * POST /api/heicode-resources/grants → /api/resource-grants + * GET /api/heicode-resources/grants/:id → /api/resource-grants/:id + * DELETE /api/heicode-resources/grants/:id → /api/resource-grants/:id + * + * Auth: pulls JWT pair from active provider's `mcpAuth`. If the access + * token is within 60s of expiry (or already expired) but refresh token is + * still valid, transparently calls mcp-server's /api/auth/refresh and + * persists the new token pair before forwarding the request. + */ + +import { ProviderService } from '../services/providerService.js' +import { ApiError, errorResponse } from '../middleware/errorHandler.js' +import type { McpAuth, SavedProvider } from '../types/provider.js' + +const providerService = new ProviderService() + +const ACCESS_REFRESH_BUFFER_MS = 60_000 // refresh if < 60s left on access token + +export async function handleHeicodeResourcesApi( + req: Request, + url: URL, + segments: string[], +): Promise { + try { + // segments: ['api', 'heicode-resources', ...rest] + const rest = segments.slice(2) + const isGrants = rest[0] === 'grants' + const upstreamBase = isGrants ? '/api/resource-grants' : '/api/resources' + const tail = isGrants ? rest.slice(1) : rest + const upstreamPath = tail.length > 0 ? `${upstreamBase}/${tail.join('/')}` : upstreamBase + + const { provider, mcpAuth } = await getActiveMcpAuth() + const freshAuth = await ensureFreshAccessToken(provider, mcpAuth) + + const upstreamUrl = + `${freshAuth.managerLoginUrl.replace(/\/+$/, '')}${upstreamPath}` + + (url.search || '') + + const headers: Record = { + Authorization: `Bearer ${freshAuth.accessToken}`, + 'X-Request-Id': crypto.randomUUID(), + } + if (req.method === 'POST' || req.method === 'PUT') { + headers['Content-Type'] = 'application/json' + } + headers['Accept'] = 'application/json' + + const init: RequestInit = { + method: req.method, + headers, + signal: AbortSignal.timeout(20_000), + } + if (req.method === 'POST' || req.method === 'PUT') { + init.body = await req.text() + } + + const upstream = await fetch(upstreamUrl, init).catch((err) => { + throw ApiError.badGateway( + `调用 Heicode Manager 失败: ${err instanceof Error ? err.message : String(err)}`, + ) + }) + + // Pass through status + body. We don't stream — payloads are small. + const body = await upstream.text() + return new Response(body, { + status: upstream.status, + headers: { + 'Content-Type': upstream.headers.get('content-type') || 'application/json', + }, + }) + } catch (err) { + return errorResponse(err) + } +} + +// ─── Helpers ────────────────────────────────────────────────────── + +async function getActiveMcpAuth(): Promise<{ provider: SavedProvider; mcpAuth: McpAuth }> { + const { providers, activeId } = await providerService.listProviders() + if (!activeId) { + throw ApiError.unauthorized('未登录 Heicode Manager。请先登录。') + } + const provider = providers.find(p => p.id === activeId) + if (!provider) { + throw ApiError.unauthorized('未找到当前活跃 provider。请重新登录。') + } + if (!provider.mcpAuth) { + throw ApiError.unauthorized( + '当前登录方式不支持资源绑定接口。请退出后用邮箱+密码登录(mcp-server 平台账号)。', + ) + } + return { provider, mcpAuth: provider.mcpAuth } +} + +async function ensureFreshAccessToken( + provider: SavedProvider, + mcpAuth: McpAuth, +): Promise { + const now = Date.now() + + // Access token still has plenty of life → use as-is. + if (mcpAuth.accessExpiresAt - now > ACCESS_REFRESH_BUFFER_MS) { + return mcpAuth + } + + // Refresh token also expired → user must re-login. + if (mcpAuth.refreshExpiresAt - now <= 0) { + throw ApiError.unauthorized( + 'Heicode Manager 登录会话已过期(refresh token 失效)。请退出重新登录。', + ) + } + + // Refresh. + const refreshUrl = `${mcpAuth.managerLoginUrl.replace(/\/+$/, '')}/api/auth/refresh` + const res = await fetch(refreshUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${mcpAuth.refreshToken}`, + }, + redirect: 'manual', + signal: AbortSignal.timeout(15_000), + }).catch((err) => { + throw ApiError.badGateway( + `刷新 Manager token 失败: ${err instanceof Error ? err.message : String(err)}`, + ) + }) + + let body: { + success?: boolean + message?: string | null + data?: { token?: string; refreshToken?: string } + } = {} + try { body = (await res.json()) as typeof body } catch { /* */ } + if (!body.success || !body.data?.token || !body.data.refreshToken) { + throw ApiError.unauthorized( + `刷新 Manager token 失败: ${body.message || `HTTP ${res.status}`}。请退出重新登录。`, + ) + } + + // Re-decode exp claims. + const newAccess = body.data.token + const newRefresh = body.data.refreshToken + const accessExp = decodeJwtExpMs(newAccess) + const refreshExp = decodeJwtExpMs(newRefresh) + const refreshed: McpAuth = { + ...mcpAuth, + accessToken: newAccess, + refreshToken: newRefresh, + accessExpiresAt: accessExp ?? Date.now() + 24 * 3600 * 1000, + refreshExpiresAt: refreshExp ?? Date.now() + 7 * 24 * 3600 * 1000, + } + + // Persist back to providers.json so the next request reuses the new token. + await providerService.updateProvider(provider.id, { mcpAuth: refreshed }) + return refreshed +} + +function decodeJwtExpMs(jwt: string): number | null { + try { + const parts = jwt.split('.') + if (parts.length !== 3) return null + const payloadB64 = parts[1]! + const base64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/') + const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4) + const json = Buffer.from(padded, 'base64').toString('utf-8') + const claims = JSON.parse(json) as { exp?: number } + if (typeof claims.exp !== 'number') return null + return claims.exp * 1000 + } catch { + return null + } +} diff --git a/cc-haha/src/server/middleware/errorHandler.ts b/cc-haha/src/server/middleware/errorHandler.ts index 485ce66..060112a 100644 --- a/cc-haha/src/server/middleware/errorHandler.ts +++ b/cc-haha/src/server/middleware/errorHandler.ts @@ -16,6 +16,10 @@ export class ApiError extends Error { return new ApiError(400, message, 'BAD_REQUEST') } + static unauthorized(message: string) { + return new ApiError(401, message, 'UNAUTHORIZED') + } + static notFound(message: string) { return new ApiError(404, message, 'NOT_FOUND') } @@ -27,6 +31,10 @@ export class ApiError extends Error { static internal(message: string) { return new ApiError(500, message, 'INTERNAL_ERROR') } + + static badGateway(message: string) { + return new ApiError(502, message, 'BAD_GATEWAY') + } } export function errorResponse(error: unknown): Response { diff --git a/cc-haha/src/server/router.ts b/cc-haha/src/server/router.ts index 6c17143..079b994 100644 --- a/cc-haha/src/server/router.ts +++ b/cc-haha/src/server/router.ts @@ -19,6 +19,7 @@ import { handleSkillsApi } from './api/skills.js' import { handleComputerUseApi } from './api/computer-use.js' import { handleHahaOAuthApi } from './api/haha-oauth.js' import { handleHeicodeAuthApi } from './api/heicode-auth.js' +import { handleHeicodeResourcesApi } from './api/heicode-resources.js' import { handleMcpApi } from './api/mcp.js' export async function handleApiRequest(req: Request, url: URL): Promise { @@ -76,6 +77,9 @@ export async function handleApiRequest(req: Request, url: URL): Promise + export const SavedProviderSchema = z.object({ id: z.string(), presetId: z.string(), @@ -30,6 +51,7 @@ export const SavedProviderSchema = z.object({ apiFormat: ApiFormatSchema.default('anthropic'), models: ModelMappingSchema, notes: z.string().optional(), + mcpAuth: McpAuthSchema.optional(), }) export const ProvidersIndexSchema = z.object({ @@ -45,6 +67,7 @@ export const CreateProviderSchema = z.object({ apiFormat: ApiFormatSchema.default('anthropic'), models: ModelMappingSchema, notes: z.string().optional(), + mcpAuth: McpAuthSchema.optional(), }) export const UpdateProviderSchema = z.object({ @@ -54,6 +77,7 @@ export const UpdateProviderSchema = z.object({ apiFormat: ApiFormatSchema.optional(), models: ModelMappingSchema.optional(), notes: z.string().optional(), + mcpAuth: McpAuthSchema.optional(), }) export const TestProviderSchema = z.object({