Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NeoLend Morning / Afternoon / Evening Work Planner (Demo)

A single-agent orchestration demo that fans out across four real MCP (Model Context Protocol) servers — Outlook, Teams, Slack, and a Task Tracker — to answer one simple question: "What should I focus on right now?" Built with FastAPI + the official MCP Python SDK + OpenAI on the backend, a React/Copilot-styled UI on the frontend, fully containerized with Docker.

Scope note: This is a demo/prototype proving the MCP orchestration pattern and user experience before building the real thing inside M365 Copilot (Microsoft Graph + Teams + Copilot Studio connectors). Login and the four tools' underlying data are simulated with local fixtures; the MCP protocol, the parallel tool-calling, and the LLM reasoning are real.


1. Problem Statement

A knowledge worker's day is fragmented across four or five different apps — a calendar and inbox, a chat tool, a team channel, a ticket tracker — and "what should I actually do first" requires manually checking all of them and mentally cross-referencing what matters. In a regulated financial services setting this is worse: a compliance deadline buried in an email, a client escalation in a Slack channel, and a board-level ask in a Teams mention can all be sitting unread at the same time, with no single view connecting them by urgency.

Building a bespoke integration for each of these tools (custom auth, custom API client, custom data shape per tool) doesn't scale as the number of connected tools grows. MCP solves the integration half of this problem: it gives an agent one standard way to discover what a tool can do and call it, regardless of which tool it is.

2. Use Case

NeoLend (fictional digital lending fintech, same company as Demo 1). Priya Shah, Regional Credit Manager, starts her day fragmented across Outlook, Teams, Slack, and an internal Task Tracker. She asks Copilot: "What should I focus on this morning?" — and again in the afternoon and evening, as the picture evolves.

A second persona, Ankit Verma (Junior Credit Analyst), has only Outlook and the Task Tracker connected — Teams and Slack are not authorized for him. This demonstrates that connector access is per-user, per-tool, exactly as M365 Copilot connectors work: the agent doesn't just have "more or less data," it structurally cannot query a tool it isn't authorized for.

Full seed data (calendar, emails, mentions, messages, tickets) lives in backend/app/data/.

3. Value Proposition

For Value
Priya (end user) One question, one place, instead of checking 4 apps every morning/afternoon/evening
Compliance/Risk Audit-critical deadlines (KYC re-verification) and client escalations surface automatically, ranked by real urgency, not by which app happened to ping loudest
Leadership A board-level Teams mention doesn't get lost in the flow of everyday chat
IT/Security Demonstrates that connector access is structural (per-tool authorization), not a UI toggle
Engineering (the MCP thesis) Proves that adding a 5th tool tomorrow means writing one more MCP server, not rewriting the orchestrator - the "brain" doesn't change

4. Solution Overview

Discover → Fan out → Reason. One orchestrator ("the brain") that:

  1. Looks up which of the 4 tools this persona has connected.
  2. For each connected tool, spawns that tool's real MCP server (via the official mcp Python SDK, stdio transport) and calls list_tools() / call_tool() — the actual MCP protocol handshake, not a mocked abstraction.
  3. Runs all connected tool calls in parallel (asyncio.gather).
  4. Feeds everything gathered, plus the requested time of day, into one reasoning LLM call that produces a ranked, time-aware plan.
image

Why real MCP, not a mocked abstraction

Each of the 4 tools is a standalone Python process speaking MCP over stdio (app/mcp_servers/*.py), using the official mcp SDK's Server class. The orchestrator (mcp_client_manager.py) is a real mcp.ClientSession - it doesn't know in advance what tools each server exposes; it calls list_tools() at runtime and gets back real tool schemas. Only the data behind each tool is fake (a JSON fixture instead of a live Microsoft Graph / Slack Web API call) - the protocol, discovery, and invocation are genuine, so the swap-in path to production is a data-source change, not a redesign.

The evolving-day mechanic

Each fixture item (email, ticket, message) carries a resolved_at / acknowledged_at timestamp (or null if still open). The three time-of-day options (morning, afternoon, evening) map to three points in a single simulated day (data/simulated_clock.json). Each MCP tool computes a simple status ("open"/"resolved") by comparing that timestamp to the requested "as of" time - the same underlying dataset naturally produces a different, evolving picture across the day, and the reasoning LLM call is the one that decides what's actually urgent given that status, not a hardcoded rule.

Per-connector authorization

mock_auth.py holds a persona -> connected_tools map. The MCP client manager only spawns a server if the persona has that tool connected - an unauthorized tool's server process is never even started. This is visible on the System Flow page's Authorization panel and in the connector strip (greyed "Not connected" state) rather than being a same-data-minus-filtering illusion.

Observability: System Flow page

Shows, live via Server-Sent Events:

  1. Per-connector authorization - which of the 4 tools this persona has allowed.
  2. MCP discovery - the actual tool schemas returned by each connected server's real list_tools() call.
  3. Connector fan-out graph - 4 parallel lanes (not a sequential chain, unlike Demo 1) showing each MCP call's status and latency as it resolves.
  4. Reasoning call panel - the single synthesis LLM call's model, tokens, latency, cost, and LangSmith trace link if configured.
  5. Run summary - total wall-clock time (should be close to the slowest single MCP call plus the reasoning call, since the fan-out is parallel, not additive), total tokens, total cost, LangSmith status.

LangSmith is optional, same pattern as Demo 1: if LANGCHAIN_API_KEY / LANGCHAIN_TRACING_V2=true are set, the reasoning call is wrapped with LangSmith's standalone traceable decorator and a trace link appears. If unset, the same latency/token/cost telemetry is still computed directly from the OpenAI response, just without a trace link.


5. Repository Layout

neolend-morning-planner/
├── backend/
│   ├── app/
│   │   ├── main.py                        # FastAPI routes
│   │   ├── config.py                      # .env-driven settings
│   │   ├── auth/mock_auth.py              # persona login + per-connector authorization
│   │   ├── mcp_servers/
│   │   │   ├── _common.py                 # shared fixture loading + status computation
│   │   │   ├── outlook_server.py          # real MCP server (stdio)
│   │   │   ├── teams_server.py
│   │   │   ├── slack_server.py
│   │   │   └── tracker_server.py
│   │   ├── orchestration/
│   │   │   ├── mcp_client_manager.py      # spawns servers, discovery, parallel tool calls
│   │   │   ├── brain.py                   # fan-out + reasoning "brain"
│   │   │   ├── telemetry.py               # cost calc + optional LangSmith wrapper
│   │   │   └── state.py                   # last-run cache (System Flow replay)
│   │   ├── prompts/*.txt                  # synthesize_plan.txt, chat_followup.txt
│   │   ├── models/schemas.py
│   │   ├── services/openai_client.py
│   │   └── data/                          # outlook/teams/slack/tracker/users/pricing/clock JSON
│   ├── requirements.txt
│   ├── Dockerfile
│   └── .env.example
├── frontend/
│   ├── src/
│   │   ├── pages/ (Login, Planner, SystemFlow)
│   │   ├── components/ (ConnectorStrip, PlanCards, ChatRail, AppShell,
│   │   │                DiscoveryPanel, FanOutGraph, ReasoningPanel,
│   │   │                AuthorizationPanel, RunSummaryFooter, toolMeta.js)
│   │   ├── api/client.js
│   │   ├── context/SessionContext.jsx
│   │   └── styles/theme.css
│   ├── package.json
│   ├── Dockerfile
│   ├── nginx.conf
│   └── .env.example
├── docker-compose.yml
├── .env.example
└── README.md   (this file)

6. Engineering Runbook

Prerequisites

  • Docker + Docker Compose (recommended), or Python 3.12 + Node.js 20 for local dev
  • An OpenAI API key
  • (Optional) A LangSmith API key

Quick start (Docker)

git clone <this-repo>
cd neolend-morning-planner

cp .env.example .env
# edit .env and set OPENAI_API_KEY=sk-...

docker-compose up --build

Sign in as Priya Shah for the full 4-tool experience. Sign in as Ankit Verma to see the per-connector authorization boundary (Teams/Slack show "Not connected" and are never queried).

Local dev (without Docker)

Backend:

cd backend
python3.12 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env               # edit OPENAI_API_KEY
uvicorn app.main:app --reload --port 8000

Frontend:

cd frontend
cp .env.example .env               # VITE_API_BASE_URL=http://localhost:8000
npm install
npm run dev                        # http://localhost:3000

Enabling LangSmith (optional)

  1. Get an API key from https://smith.langchain.com
  2. In .env:
    LANGCHAIN_TRACING_V2=true
    LANGCHAIN_API_KEY=ls__your-key
    LANGCHAIN_PROJECT=neolend-morning-planner-demo
    
  3. Restart the backend. The System Flow page's Reasoning panel will now show a "View trace ↗" link, and the Run Summary will link to the LangSmith project dashboard.
  4. No key set → identical behavior, minus the trace link.

Adding a 5th MCP-connected tool

This is the demo's core extensibility story - notice how little of the orchestrator needs to change:

  1. Write a new app/mcp_servers/<tool>_server.py following the existing pattern (list_tools() + call_tool() handlers, backed by a JSON fixture).
  2. Add its fixture file to app/data/.
  3. Register it in mcp_client_manager.py's SERVER_SCRIPTS, SERVER_DISPLAY_NAMES, and TOOL_CALL_PLAN dicts.
  4. Add it to ALL_TOOLS in brain.py and main.py.
  5. Add it to the persona's connected_tools list in users.json if authorized.
  6. Add it to TOOLS in frontend/src/components/toolMeta.js for the UI.

brain.py's fan-out and reasoning logic itself does not change - this is the payoff of building on MCP's uniform tool-calling interface instead of bespoke per-tool code.

Adding a new persona / adjusting the day's data

  • New persona: add an entry to backend/app/data/users.json with a connected_tools list.
  • Adjust urgency over the day: edit resolved_at / acknowledged_at timestamps in the fixture files, or the three as_of timestamps in simulated_clock.json.
  • No code changes needed - the MCP servers, authorization, and reasoning all read from these files.

Troubleshooting

Symptom Likely cause Fix
401 Missing session token Frontend didn't send x-session-token Confirm login succeeded; check SessionContext
CORS error in browser console CORS_ORIGINS doesn't include the frontend's origin Add the exact origin to CORS_ORIGINS in .env
Plan run hangs on a connector MCP subprocess failed to start (e.g. Python path issue in a custom Docker base image) Check backend container logs; the connector's SSE event will carry an error field
OpenAI 401/403 errors OPENAI_API_KEY missing/invalid Check .env, restart backend
A connector is always skipped for a user who should have it connected_tools in users.json doesn't list that tool for that persona Add the tool key to their connected_tools array
System Flow page empty on first visit No plan has been run yet Click "Run live", or run a plan from the Planner page first
Reasoning output has _parse_warning / empty plan Model didn't return valid JSON Check synthesize_plan.txt wording; the pipeline degrades gracefully rather than crashing

7. What's Simulated vs Real

Component Real Simulated
MCP protocol (discovery + tool calls) ✅ Official MCP SDK, real stdio handshake
Parallel fan-out across connectors ✅ Real asyncio.gather over real MCP sessions
Per-connector authorization ✅ Real check; unauthorized servers are never spawned Authorization map itself is a static fixture, not real OAuth
LLM reasoning (synthesis call) ✅ Real OpenAI call
Latency, token counts, cost ✅ Computed from real OpenAI response + real MCP call timing
LangSmith tracing ✅ Real, if configured
Login Persona picker, no real Entra ID/SSO
Outlook/Teams/Slack/Tracker data Static JSON fixtures behind each MCP server, not live APIs
Chat "draft a reply" Produces a draft only; nothing is actually sent

8. Path to Production (M365 Copilot)

  • Each *_server.py swaps its load_fixture() calls for real Microsoft Graph / Slack Web API / Jira-or-equivalent calls - the MCP tool names/schemas can stay the same.
  • mock_auth.py's connected_tools map becomes a real query against each connector's OAuth consent state (Microsoft Graph connectors, Slack app installs, etc.).
  • mcp_client_manager.py and brain.py need no changes - this is the point of building on MCP: the orchestrator's fan-out/reasoning logic is decoupled from what's actually behind each tool.
  • Persona login → Entra ID / SSO.

About

FocusPilot is an AI-powered workday planner that leverages Model Context Protocol (MCP) to gather context from multiple enterprise tools and generate intelligent, prioritized work plans with actionable recommendations.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages