MCP Servers and Dify AI Integration¶
Yaya Engine exposes two Model Context Protocol servers, and they exist for opposite reasons: YayaServer gives an external AI agent (Dify) a narrow, machine-authenticated slice of write access to user actions, while InternalYayaServer gives the Atram engineering team a much wider, OAuth-authenticated read/write window into operational data for use with Claude Desktop or Claude Code. Both are registered in routes/ai.php using the laravel/mcp package's Mcp::web() helper, and both are distinct from routes/mcp.php, which only handles the login session for the /mcp admin web UI [@ai-php] [@mcp-php]. Understanding this pair of servers means understanding not just what tools they expose, but why the AI conversation loop that calls them is simpler — and less guarded — than the project's own documentation claims.
YayaServer: three tools, one write path, for Dify¶
YayaServer is mounted at Mcp::web('/mcp/yaya') behind auth.admin_token middleware — the same shared-bearer-token check used by the service-to-service /api/v1 surface, not a user session [@yaya-server]. It exposes exactly three tools, all under app/Mcp/Tools/: GetUserActionsTool and GetUserActionDetailsTool for reads, and UpdateActionStepStatusTool as the only write path. UpdateActionStepStatusTool completes a UserActionStep through UserActionService::completeStep() when evidence is not required or has already been approved, and it fires two Mixpanel events per call — MCP Tool Called and, on success, MCP Step Status Updated — scoped to the affected user rather than to an admin [@update-step-tool]. This is deliberately the smallest possible surface: Dify only needs to read a user's active tasks and mark simple steps done, so the tool count stays at three even as the internal server has grown to twenty.
InternalYayaServer: twenty tools, gated per-tool, for the team¶
InternalYayaServer is mounted at Mcp::web('/mcp/internal-yaya') behind ['auth:api', 'can:mcp.access'] — the Passport-backed api guard described in Auth architecture — and additionally exposes Laravel MCP's OAuth routes (Mcp::oauthRoutes()) so a Claude Desktop/Code client can authenticate interactively rather than presenting a bearer token directly [@internal-yaya-server] [@ai-php]. Its twenty tools span user search and profiles, channel accounts, conversations, alert summaries and per-alert delivery reports, the verification queue, payouts, messaging/batch statistics, and FSP summaries, plus an aggregate GetOperationsSummaryTool meant as a dashboard-style entry point. The server's own $instructions string, which the LLM sees on connection, spells out an investigation workflow: search for a user, check whether an alert reached them via the delivery report, list the users assigned to an alert, then drill into conversations — a sequence clearly written to keep an agent from guessing at tool order [@internal-yaya-server].
Every tool in this set extends an abstract InternalTool base class rather than the package's plain Tool class, and that base class is where the real access control lives [@internal-tool-base]. eligibleForRegistration() resolves the authenticated AdminUser (checking the default guard, the admin guard, and finally the api guard, in that order) and only registers the tool at all if that user can both mcp.access and the tool's own $requiredPermission — either mcp.tools.read (the default) or mcp.tools.write. Only two tools in the whole set flip $isReadOnly to false and require mcp.tools.write: ApproveEvidenceTool and RejectEvidenceTool [@approve-evidence-tool]. InternalTool::toArray() also tags every response with an MCP annotation — readOnlyHint for the eighteen read tools, destructiveHint for the two write tools — so a well-behaved MCP client can distinguish safe exploratory calls from state-changing ones before it makes them [@internal-tool-base]. ApproveEvidenceTool itself is a useful example of what a write tool looks like here: it validates the step is in EvidenceUploaded status before proceeding, delegates the actual state change to UserActionService::approveEvidence() (which can auto-complete the parent action and trigger a payout), and fires the same Mixpanel MCP Tool Called pattern used by the public server, but keyed to the approving admin rather than the affected user [@approve-evidence-tool].
Machine clients need a real Passport token to reach /mcp/internal-yaya at all. A Filament page, ManageMcpTokens, lets super-admins mint and revoke personal-access tokens scoped to service-account AdminUser records for exactly this purpose — see Mint MCP Tokens from the Filament Admin Panel for why that page exists rather than a CLI-only flow, and MCP Tool Reference for the full per-tool contract of both servers [@mcp-tokens-page].
Dify: the AI layer that actually runs, without Portkey or LangFuse in front of it¶
The conversational AI experience — the thing a WhatsApp or Telegram user is actually talking to — is not built on either MCP server directly. It runs through DifyService, which is invoked from ProcessIncomingMessage, the job that handles every inbound channel message [@process-incoming-message]. DifyService::chat() uses the Yaya User's numeric ID as the Dify session identifier specifically so a conversation started on WhatsApp and continued on Telegram stays contextually linked on Dify's side, then hands the request to DifyConnector, a Saloon HTTP client that adds a bearer Authorization header and POSTs to /v1/chat-messages on Dify's configured base URL [@dify-service] [@dify-connector]. Dify, in turn, is the one calling YayaServer's three tools when it needs to read or update a user's action state — this is the "MCP Layer" CLAUDE.md describes, just narrower than the four-tool table there implies.
What is missing is the rest of the pipeline the documentation promises: CLAUDE.md's request-flow diagram routes every AI response through Portkey for guardrails and routing, then logs every call to LangFuse for observability, alongside Mixpanel. Neither exists in code — there is no Portkey client, no LangFuse client, and no Prism PHP dependency anywhere in app/ or config/, confirmed by a repository-wide search that returns zero hits outside documentation files. DifyService calls Dify directly and logs failures to the standard Laravel log; nothing inspects, blocks, or traces the AI turn in between. Treat the guardrail/observability layer as aspirational until code says otherwise.
One control does exist and is real: a per-channel Dify kill switch. ProcessIncomingMessage checks KillSwitchSettings::isDifyEnabledFor($channel) before handing a message to Dify at all, so an operator can pause the AI hand-off for one channel (say, Telegram) without touching WhatsApp or SMS, and without the rest of message processing (delivery logging, conversation bookkeeping) breaking [@process-incoming-message] [@kill-switch-settings]. See Runtime Dify Kill Switch (Global and Per-Channel) for the reasoning behind scoping this per channel rather than globally.