Skip to content

Commit af73264

Browse files
committed
feat: ethora-b2b-app-provision orchestrator
1 parent b006b93 commit af73264

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Tip: to list runnable recipes without calling `ethora-help`, call `ethora-run-re
9999
- `ethora-app-tokens-create-v2` — create new app token (returned once) (B2B auth)
100100
- `ethora-app-tokens-rotate-v2` — rotate token (revoke old, return new once) (B2B auth)
101101
- `ethora-app-tokens-revoke-v2` — revoke token by tokenId (idempotent) (B2B auth)
102+
- `ethora-b2b-app-provision` — create app + create tokens + provision rooms + configure bot (B2B orchestrator)
102103

103104
- **Chat & Rooms**
104105
- `ethora-app-get-default-rooms` — list default rooms

src/apiClientDappros.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ export function appTokensRevokeV2(appId: string, tokenId: string, opts?: { timeo
408408
return httpClientDappros.delete(`/v2/apps/${id}/tokens/${tid}`, { timeout: opts?.timeoutMs })
409409
}
410410

411+
export function appProvisionV2(appId: string, payload: { rooms?: Array<{ title: string; pinned?: boolean }> }, opts?: { timeoutMs?: number }) {
412+
const id = String(appId || "").trim()
413+
return httpClientDappros.post(`/v2/apps/${id}/provision`, payload || {}, { timeout: opts?.timeoutMs })
414+
}
415+
411416
// files v2 (user auth)
412417
export function filesUploadV2(formData: any, headers?: any) {
413418
return httpClientDappros.post(`/v2/files`, formData, { headers })

src/tools.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp"
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types"
33
import z from "zod"
4-
import { appCreate, appCreateChat, appDelete, appDeleteChat, appGetDefaultRooms, appGetDefaultRoomsWithAppId, appList, appTokensCreateV2, appTokensListV2, appTokensRevokeV2, appTokensRotateV2, appUpdate, apiPing, botGetV2, botUpdateV2, chatsBroadcastJobV2, chatsBroadcastV2, configureB2BToken, configureClient, filesDeleteV2, filesGetV2, filesUploadV2, getClientState, selectApp, setAuthMode, sourcesDocsDelete, sourcesDocsDeleteV2, sourcesDocsUpload, sourcesDocsUploadV2, sourcesSiteCrawl, sourcesSiteCrawlV2, sourcesSiteDeleteUrl, sourcesSiteDeleteUrlV2, sourcesSiteDeleteUrlV2Batch, sourcesSiteDeleteUrlV2Single, sourcesSiteReindex, sourcesSiteReindexV2, userLogin, userRegistration, usersBatchCreateJobV2, usersBatchCreateV2, walletERC20Transfer, walletGetBalance } from "./apiClientDappros.js"
4+
import { appCreate, appCreateChat, appDelete, appDeleteChat, appGetDefaultRooms, appGetDefaultRoomsWithAppId, appList, appProvisionV2, appTokensCreateV2, appTokensListV2, appTokensRevokeV2, appTokensRotateV2, appUpdate, apiPing, botGetV2, botUpdateV2, chatsBroadcastJobV2, chatsBroadcastV2, configureB2BToken, configureClient, filesDeleteV2, filesGetV2, filesUploadV2, getClientState, selectApp, setAuthMode, sourcesDocsDelete, sourcesDocsDeleteV2, sourcesDocsUpload, sourcesDocsUploadV2, sourcesSiteCrawl, sourcesSiteCrawlV2, sourcesSiteDeleteUrl, sourcesSiteDeleteUrlV2, sourcesSiteDeleteUrlV2Batch, sourcesSiteDeleteUrlV2Single, sourcesSiteReindex, sourcesSiteReindexV2, userLogin, userRegistration, usersBatchCreateJobV2, usersBatchCreateV2, walletERC20Transfer, walletGetBalance } from "./apiClientDappros.js"
55
import { fail, ok } from "./mcpResponse.js"
66

77
function errorToText(error: unknown) {
@@ -2252,6 +2252,94 @@ function appTokensRevokeV2Tool(server: McpServer) {
22522252
)
22532253
}
22542254

2255+
function b2bAppProvisionTool(server: McpServer) {
2256+
server.registerTool(
2257+
"ethora-b2b-app-provision",
2258+
{
2259+
description: "One-call B2B flow: create app → create one or more app tokens → provision default rooms → configure/enable bot.",
2260+
inputSchema: {
2261+
displayName: z.string().min(1).describe("New app display name"),
2262+
// tokens
2263+
tokenLabels: z.array(z.string().min(1)).min(1).max(5).optional().describe("Optional labels for new tokens. Default: ['default']"),
2264+
// rooms
2265+
rooms: z.array(z.object({
2266+
title: z.string().min(1),
2267+
pinned: z.boolean().optional(),
2268+
})).max(20).optional().describe("Optional default rooms to create (B2B)."),
2269+
// bot
2270+
enableBot: z.boolean().optional().describe("If true, enables bot (app-token auth using first created token)."),
2271+
botTrigger: z.enum(["any_message", "/bot"]).optional(),
2272+
botPrompt: z.string().optional(),
2273+
botGreetingMessage: z.string().optional(),
2274+
},
2275+
},
2276+
async function ({ displayName, tokenLabels, rooms, enableBot, botTrigger, botPrompt, botGreetingMessage }) {
2277+
const meta = getDefaultMeta("ethora-b2b-app-provision")
2278+
const prev = (getClientState() as any).authMode as any
2279+
try {
2280+
ensureB2BAuthForTool()
2281+
2282+
const steps: any[] = []
2283+
2284+
// 1) Create app (B2B)
2285+
setAuthMode("b2b")
2286+
const created = await appCreate(displayName)
2287+
const app = created?.data?.app
2288+
const appId = String(app?._id || app?.id || "").trim()
2289+
if (!appId) throw new Error("Create app succeeded but no appId found in response")
2290+
steps.push({ step: "appCreate", ok: true, appId })
2291+
2292+
// 2) Create tokens (B2B)
2293+
const labels = Array.isArray(tokenLabels) && tokenLabels.length ? tokenLabels : ["default"]
2294+
const createdTokens: any[] = []
2295+
for (const label of labels) {
2296+
const r = await appTokensCreateV2(appId, { label }, { timeoutMs: 10_000 })
2297+
createdTokens.push(r.data)
2298+
}
2299+
const primaryToken = String(createdTokens?.[0]?.token || "").trim()
2300+
steps.push({ step: "appTokensCreate", ok: true, count: createdTokens.length })
2301+
2302+
// 3) Provision rooms (B2B)
2303+
let provisionRes: any = null
2304+
if (Array.isArray(rooms) && rooms.length) {
2305+
const r = await appProvisionV2(appId, { rooms }, { timeoutMs: 60_000 })
2306+
provisionRes = r.data
2307+
steps.push({ step: "appProvisionRooms", ok: true, requested: rooms.length })
2308+
}
2309+
2310+
// 4) Configure bot (app-token using first created token)
2311+
let botRes: any = null
2312+
if (enableBot || botTrigger || botPrompt || botGreetingMessage) {
2313+
if (!primaryToken) throw new Error("Bot config requested but no app token was created")
2314+
selectApp({ appId, appToken: primaryToken, authMode: "app" })
2315+
setAuthMode("app")
2316+
const payload: any = {}
2317+
if (enableBot) payload.status = "on"
2318+
if (botTrigger) payload.trigger = botTrigger
2319+
if (botPrompt) payload.prompt = botPrompt
2320+
if (botGreetingMessage) payload.greetingMessage = botGreetingMessage
2321+
const r = await botUpdateV2(payload)
2322+
botRes = r.data
2323+
steps.push({ step: "botUpdateV2", ok: true })
2324+
}
2325+
2326+
return asToolResult(ok({
2327+
appId,
2328+
app,
2329+
tokens: createdTokens,
2330+
provision: provisionRes,
2331+
bot: botRes,
2332+
steps,
2333+
state: getClientState(),
2334+
}, meta))
2335+
} catch (error) {
2336+
try { setAuthMode(prev) } catch (_) {}
2337+
return asToolResult(fail(error, meta))
2338+
}
2339+
}
2340+
)
2341+
}
2342+
22552343
function sourcesSiteDeleteUrlV2AppTool(server: McpServer) {
22562344
server.registerTool(
22572345
"ethora-sources-site-delete-url-v2",
@@ -2385,6 +2473,7 @@ export function registerTools(server: McpServer) {
23852473
appTokensCreateV2Tool(server);
23862474
appTokensRotateV2Tool(server);
23872475
appTokensRevokeV2Tool(server);
2476+
b2bAppProvisionTool(server);
23882477
userLoginWithEmailTool(server);
23892478
userRegisterWithEmailTool(server);
23902479
appListTool(server);

0 commit comments

Comments
 (0)