11import { McpServer } from "@modelcontextprotocol/sdk/server/mcp"
22import { CallToolResult } from "@modelcontextprotocol/sdk/types"
33import z from "zod"
4- import { appCreate , appCreateChat , appDelete , appDeleteChat , appGetDefaultRooms , appGetDefaultRoomsWithAppId , appList , appUpdate , apiPing , chatsBroadcastJobV2 , chatsBroadcastV2 , configureClient , filesDeleteV2 , filesGetV2 , filesUploadV2 , getClientState , selectApp , setAuthMode , sourcesDocsDelete , sourcesDocsDeleteV2 , sourcesDocsUpload , sourcesDocsUploadV2 , sourcesSiteCrawl , sourcesSiteCrawlV2 , sourcesSiteDeleteUrl , sourcesSiteDeleteUrlV2 , sourcesSiteDeleteUrlV2Batch , sourcesSiteDeleteUrlV2Single , sourcesSiteReindex , sourcesSiteReindexV2 , userLogin , userRegistration , walletERC20Transfer , walletGetBalance } from "./apiClientDappros.js"
4+ import { appCreate , appCreateChat , appDelete , appDeleteChat , appGetDefaultRooms , appGetDefaultRoomsWithAppId , appList , appUpdate , apiPing , chatsBroadcastJobV2 , chatsBroadcastV2 , configureB2BToken , configureClient , filesDeleteV2 , filesGetV2 , filesUploadV2 , getClientState , selectApp , setAuthMode , sourcesDocsDelete , sourcesDocsDeleteV2 , sourcesDocsUpload , sourcesDocsUploadV2 , sourcesSiteCrawl , sourcesSiteCrawlV2 , sourcesSiteDeleteUrl , sourcesSiteDeleteUrlV2 , sourcesSiteDeleteUrlV2Batch , sourcesSiteDeleteUrlV2Single , sourcesSiteReindex , sourcesSiteReindexV2 , userLogin , userRegistration , walletERC20Transfer , walletGetBalance } from "./apiClientDappros.js"
55import { fail , ok } from "./mcpResponse.js"
66
77function errorToText ( error : unknown ) {
@@ -68,12 +68,16 @@ function configureTool(server: McpServer) {
6868 inputSchema : {
6969 apiUrl : z . string ( ) . optional ( ) . describe ( "Ethora API URL (e.g. https://api.ethora.com/v1 or http://localhost:8080/v1)" ) ,
7070 appJwt : z . string ( ) . optional ( ) . describe ( "Ethora App JWT (used for login/register endpoints)." ) ,
71+ b2bToken : z . string ( ) . optional ( ) . describe ( "Ethora B2B token for x-custom-token auth (type=server)." ) ,
7172 } ,
7273 } ,
73- async function ( { apiUrl, appJwt } ) {
74+ async function ( { apiUrl, appJwt, b2bToken } ) {
7475 try {
7576 const state = configureClient ( { apiUrl, appJwt } )
76- return asToolResult ( ok ( state , getDefaultMeta ( "ethora-configure" ) ) )
77+ if ( typeof b2bToken === "string" ) {
78+ configureB2BToken ( b2bToken )
79+ }
80+ return asToolResult ( ok ( getClientState ( ) , getDefaultMeta ( "ethora-configure" ) ) )
7781 } catch ( error ) {
7882 return asToolResult ( fail ( error , getDefaultMeta ( "ethora-configure" ) ) )
7983 }
@@ -212,6 +216,32 @@ function authUseUserTool(server: McpServer) {
212216 )
213217}
214218
219+ function authUseB2BTool ( server : McpServer ) {
220+ server . registerTool (
221+ "ethora-auth-use-b2b" ,
222+ {
223+ description : "Switch auth mode to B2B (x-custom-token) for subsequent API calls (requires b2bToken)." ,
224+ } ,
225+ async function ( ) {
226+ try {
227+ return asToolResult ( ok ( setAuthMode ( "b2b" ) , getDefaultMeta ( "ethora-auth-use-b2b" ) ) )
228+ } catch ( error ) {
229+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora-auth-use-b2b" ) ) )
230+ }
231+ }
232+ )
233+ }
234+
235+ function ensureB2BAuthForTool ( ) {
236+ const state = getClientState ( ) as any
237+ if ( state . authMode !== "b2b" ) {
238+ throw new Error ( "This tool requires B2B auth. Call `ethora-auth-use-b2b` (and configure b2bToken via `ethora-configure`) first." )
239+ }
240+ if ( ! state . hasB2BToken ) {
241+ throw new Error ( "B2B auth mode selected, but b2bToken is missing. Provide it via env ETHORA_B2B_TOKEN or `ethora-configure`." )
242+ }
243+ }
244+
215245function appSelectTool ( server : McpServer ) {
216246 server . registerTool (
217247 "ethora-app-select" ,
@@ -284,6 +314,46 @@ function chatsBroadcastJobTool(server: McpServer) {
284314 )
285315}
286316
317+ function sleep ( ms : number ) {
318+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
319+ }
320+
321+ function waitBroadcastJobTool ( server : McpServer ) {
322+ server . registerTool (
323+ "ethora-wait-broadcast-job-v2" ,
324+ {
325+ description : "Poll broadcast job status until completed/failed (app-token auth)." ,
326+ inputSchema : {
327+ jobId : z . string ( ) . min ( 1 ) ,
328+ timeoutMs : z . number ( ) . int ( ) . min ( 1000 ) . max ( 300000 ) . optional ( ) . describe ( "Max wait time (default 60000)" ) ,
329+ intervalMs : z . number ( ) . int ( ) . min ( 250 ) . max ( 10000 ) . optional ( ) . describe ( "Poll interval (default 1000)" ) ,
330+ } ,
331+ } ,
332+ async function ( { jobId, timeoutMs, intervalMs } ) {
333+ const meta = getDefaultMeta ( "ethora-wait-broadcast-job-v2" )
334+ try {
335+ ensureAppAuthForTool ( )
336+ const timeout = timeoutMs ?? 60_000
337+ const interval = intervalMs ?? 1_000
338+ const started = Date . now ( )
339+ let last : any = null
340+ while ( Date . now ( ) - started < timeout ) {
341+ const res = await chatsBroadcastJobV2 ( jobId )
342+ last = res . data
343+ const state = String ( last ?. state || "" )
344+ if ( state === "completed" || state === "failed" ) {
345+ return asToolResult ( ok ( { done : true , state, job : last } , meta ) )
346+ }
347+ await sleep ( interval )
348+ }
349+ return asToolResult ( ok ( { done : false , reason : "timeout" , job : last } , meta ) )
350+ } catch ( error ) {
351+ return asToolResult ( fail ( error , meta ) )
352+ }
353+ }
354+ )
355+ }
356+
287357function filesUploadV2Tool ( server : McpServer ) {
288358 server . registerTool (
289359 "ethora-files-upload-v2" ,
@@ -623,7 +693,7 @@ function appUpdateTool(server: McpServer) {
623693 domainName : z . string ( ) . optional ( ) . describe ( "If the domainName is set to 'abcd', your web application will be available at abcd.ethora.com." ) ,
624694 appDescription : z . string ( ) . optional ( ) . describe ( "Set the application description" ) ,
625695 primaryColor : z . string ( ) . optional ( ) . describe ( "Set thie color of the application in #F54927 format" ) ,
626- botStatus : z . enum ( [ "on" , "off" ] ) . describe ( "Set the bot status to on or off, if on bot is enabled" )
696+ botStatus : z . enum ( [ "on" , "off" ] ) . optional ( ) . describe ( "Set the bot status to on or off, if on bot is enabled" )
627697 }
628698 } ,
629699 async function ( { appId, displayName, domainName, appDescription, primaryColor, botStatus } ) {
@@ -792,6 +862,130 @@ function walletERC20TransferTool(server: McpServer) {
792862 )
793863}
794864
865+ function b2bAppCreateTool ( server : McpServer ) {
866+ server . registerTool (
867+ "ethora-b2b-app-create" ,
868+ {
869+ description : "Create a new app using B2B auth (x-custom-token)." ,
870+ inputSchema : {
871+ displayName : z . string ( ) . min ( 1 ) ,
872+ } ,
873+ } ,
874+ async function ( { displayName } ) {
875+ try {
876+ ensureB2BAuthForTool ( )
877+ const res = await appCreate ( displayName )
878+ return asToolResult ( ok ( res . data , getDefaultMeta ( "ethora-b2b-app-create" ) ) )
879+ } catch ( error ) {
880+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora-b2b-app-create" ) ) )
881+ }
882+ }
883+ )
884+ }
885+
886+ function b2bBotEnableTool ( server : McpServer ) {
887+ server . registerTool (
888+ "ethora-b2b-bot-enable" ,
889+ {
890+ description : "Enable AI bot for an app (B2B auth). This triggers backend best-effort activation against configured AI service." ,
891+ inputSchema : {
892+ appId : z . string ( ) . optional ( ) . describe ( "Defaults to current app if selected" ) ,
893+ botTrigger : z . string ( ) . optional ( ) . describe ( "Optional bot trigger (example: '/bot' or 'any_message')" ) ,
894+ } ,
895+ } ,
896+ async function ( { appId, botTrigger } ) {
897+ try {
898+ ensureB2BAuthForTool ( )
899+ const state = getClientState ( ) as any
900+ const effectiveAppId = appId || state . currentAppId
901+ if ( ! effectiveAppId ) {
902+ throw new Error ( "appId is required (pass appId or call `ethora-app-select` first)" )
903+ }
904+ const changes : any = { botStatus : "on" }
905+ if ( botTrigger ) changes . botTrigger = botTrigger
906+ const res = await appUpdate ( effectiveAppId , changes )
907+ return asToolResult ( ok ( res . data , getDefaultMeta ( "ethora-b2b-bot-enable" ) ) )
908+ } catch ( error ) {
909+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora-b2b-bot-enable" ) ) )
910+ }
911+ }
912+ )
913+ }
914+
915+ // Minimal namespace aliases to reduce auth-mode mistakes for agents.
916+ function b2bAliases ( server : McpServer ) {
917+ server . registerTool (
918+ "ethora.b2b.auth.use" ,
919+ { description : "Alias for ethora-auth-use-b2b" } ,
920+ async function ( ) {
921+ try {
922+ return asToolResult ( ok ( setAuthMode ( "b2b" ) , getDefaultMeta ( "ethora.b2b.auth.use" ) ) )
923+ } catch ( error ) {
924+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora.b2b.auth.use" ) ) )
925+ }
926+ }
927+ )
928+ server . registerTool (
929+ "ethora.b2b.app.create" ,
930+ { description : "Alias for ethora-b2b-app-create" , inputSchema : { displayName : z . string ( ) . min ( 1 ) } } ,
931+ async function ( { displayName } ) {
932+ try {
933+ ensureB2BAuthForTool ( )
934+ const res = await appCreate ( displayName )
935+ return asToolResult ( ok ( res . data , getDefaultMeta ( "ethora.b2b.app.create" ) ) )
936+ } catch ( error ) {
937+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora.b2b.app.create" ) ) )
938+ }
939+ }
940+ )
941+ server . registerTool (
942+ "ethora.b2b.bot.enable" ,
943+ { description : "Alias for ethora-b2b-bot-enable" , inputSchema : { appId : z . string ( ) . optional ( ) , botTrigger : z . string ( ) . optional ( ) } } ,
944+ async function ( { appId, botTrigger } ) {
945+ try {
946+ ensureB2BAuthForTool ( )
947+ const state = getClientState ( ) as any
948+ const effectiveAppId = appId || state . currentAppId
949+ if ( ! effectiveAppId ) {
950+ throw new Error ( "appId is required (pass appId or call `ethora-app-select` first)" )
951+ }
952+ const changes : any = { botStatus : "on" }
953+ if ( botTrigger ) changes . botTrigger = botTrigger
954+ const res = await appUpdate ( effectiveAppId , changes )
955+ return asToolResult ( ok ( res . data , getDefaultMeta ( "ethora.b2b.bot.enable" ) ) )
956+ } catch ( error ) {
957+ return asToolResult ( fail ( error , getDefaultMeta ( "ethora.b2b.bot.enable" ) ) )
958+ }
959+ }
960+ )
961+ server . registerTool (
962+ "ethora.b2b.broadcast.wait" ,
963+ { description : "Alias for ethora-wait-broadcast-job-v2" , inputSchema : { jobId : z . string ( ) . min ( 1 ) , timeoutMs : z . number ( ) . int ( ) . min ( 1000 ) . max ( 300000 ) . optional ( ) , intervalMs : z . number ( ) . int ( ) . min ( 250 ) . max ( 10000 ) . optional ( ) } } ,
964+ async function ( { jobId, timeoutMs, intervalMs } ) {
965+ const meta = getDefaultMeta ( "ethora.b2b.broadcast.wait" )
966+ try {
967+ ensureAppAuthForTool ( )
968+ const timeout = timeoutMs ?? 60_000
969+ const interval = intervalMs ?? 1_000
970+ const started = Date . now ( )
971+ let last : any = null
972+ while ( Date . now ( ) - started < timeout ) {
973+ const res = await chatsBroadcastJobV2 ( jobId )
974+ last = res . data
975+ const state = String ( last ?. state || "" )
976+ if ( state === "completed" || state === "failed" ) {
977+ return asToolResult ( ok ( { done : true , state, job : last } , meta ) )
978+ }
979+ await sleep ( interval )
980+ }
981+ return asToolResult ( ok ( { done : false , reason : "timeout" , job : last } , meta ) )
982+ } catch ( error ) {
983+ return asToolResult ( fail ( error , meta ) )
984+ }
985+ }
986+ )
987+ }
988+
795989function sourcesSiteCrawlV2AppTool ( server : McpServer ) {
796990 server . registerTool (
797991 "ethora-sources-site-crawl-v2" ,
@@ -938,9 +1132,11 @@ export function registerTools(server: McpServer) {
9381132 doctorTool ( server ) ;
9391133 authUseAppTool ( server ) ;
9401134 authUseUserTool ( server ) ;
1135+ authUseB2BTool ( server ) ;
9411136 appSelectTool ( server ) ;
9421137 chatsBroadcastTool ( server ) ;
9431138 chatsBroadcastJobTool ( server ) ;
1139+ waitBroadcastJobTool ( server ) ;
9441140 filesUploadV2Tool ( server ) ;
9451141 filesGetV2Tool ( server ) ;
9461142 filesDeleteV2Tool ( server ) ;
@@ -968,4 +1164,7 @@ export function registerTools(server: McpServer) {
9681164 getDefaultRoomsWithAppIdTool ( server ) ;
9691165 walletGetBalanceTool ( server ) ;
9701166 walletERC20TransferTool ( server ) ;
1167+ b2bAppCreateTool ( server ) ;
1168+ b2bBotEnableTool ( server ) ;
1169+ b2bAliases ( server ) ;
9711170}
0 commit comments