Feat/m365 mcp#83
Merged
simongdavies merged 15 commits intohyperlight-dev:mainfrom Apr 28, 2026
Merged
Conversation
- Catalog of 21 Agent 365 MCP servers (scripts/m365-mcp-servers.json),
populated from the live discoverToolServers endpoint with per-server
url + scope baked in. URL pattern is /agents/servers/<name> (verified
against discovery), not /agents/tenants/<tid>/servers/<name>.
- Cross-platform setup scripts (TypeScript via tsx; run on Linux, macOS,
WSL, Windows native, Git Bash):
- scripts/setup-m365-app.ts: create / reuse / adopt single-tenant
Entra app registration, declare per-server delegated scopes,
attempt admin consent, persist state to ~/.hyperagent/m365.json.
- scripts/m365-setup.ts: write one HTTP MCP entry per selected
service into ~/.hyperagent/config.json with narrow per-server
scopes (e.g. McpServers.Mail.All).
- scripts/m365-refresh-servers.ts: refresh the catalog from the live
discovery endpoint using a cached token.
- scripts/m365-show.ts: print the saved app details.
- scripts/mcp-add-http.ts: generic HTTP MCP entry writer (any vendor).
- Justfile recipes are now plain delegates to tsx (no [unix] attribute,
no inline bash) so they run identically across all supported shells.
- src/agent/mcp/retry-fetch.ts: HTTP retry middleware (429/502/503/504 +
network errors, exponential backoff, capped Retry-After) wired into
StreamableHTTPClientTransport via the SDK's fetch option.
- src/agent/mcp/session-cache.ts: Mcp-Session-Id persistence at
~/.hyperagent/mcp-sessions/<server>.json so reconnects can reattach
to an existing session; cleared on connect failure.
- extractContent now handles three Agent 365 response shapes via the
new extractEmbeddedJson helper: clean JSON, status-prefixed JSON
(Calendar), and {rawResponse: '...'} wrappers (Mail).
- 16 new tests covering retry-fetch behaviour and the embedded-JSON
extraction patterns. Full suite: 2197 passing.
scripts/build-modules.js was regenerating ha-modules.d.ts BEFORE running tsc. On a fresh checkout the gitignored builtin-modules/*.d.ts files don't exist, so the regenerated ha-modules.d.ts only contained the 4 native module declarations — wiping the up-to-date committed file. tsc then failed with cascading TS2305 errors: src/pptx-charts.ts: Module 'ha:ooxml-core' has no exported member '_createShapeFragment' (and similar for ShapeFragment, isShapeFragment, fragmentsToXml, MAX_CHARTS_PER_DECK, etc.) CI didn't catch this because it relied on cached / pre-built .d.ts files surviving between runs. The dts-sync test (which would catch drift) only runs after a successful build. Fix: swap the order. Run tsc first using the committed (correct) ha-modules.d.ts to emit fresh per-module .d.ts files, THEN regenerate ha-modules.d.ts from those fresh files. The regenerated output now matches committed byte-for-byte, and dts-sync.test.ts continues to catch drift.
Previously 'just clean' only removed dist/ and node_modules/. The real
landmines after a failed build are:
- gitignored builtin-modules/*.{js,d.ts} (except the committed
_save.js / _restore.js)
- gitignored plugins/*/index.d.ts and plugins/shared/*.js
- generated plugins/host-modules.d.ts and plugin-schema-types.d.ts
- a clobbered (tracked) builtin-modules/src/types/ha-modules.d.ts —
'git pull' refuses to overwrite it because it has local changes
from the broken build, so subsequent setups keep failing
The new recipe wipes all of these and restores ha-modules.d.ts from
HEAD, so a single 'just clean && just setup' recovers from any
mid-build failure on every supported platform (unix + windows
variants provided).
The Agent 365 gateway requires a tenant-scoped path:
https://<host>/agents/tenants/<tenantId>/servers/<name>
The discoverToolServers endpoint returns un-tenanted URLs of the form
https://<host>/agents/servers/<name>
because the tenant comes from the caller's context.
We were storing the discovery URL verbatim in
~/.hyperagent/config.json — which made the gateway respond:
{"code":"EndpointInvalid",
"message":"Tenant id is invalid.",
"innererror":{"code":"TenantIdInvalid"}}
(note the double space — empty tenantId substitution).
Fix: m365-setup.ts now rewrites each catalog URL at config-write time
via injectTenantIntoUrl(), splicing /tenants/<tenantId> into the path.
The catalog itself still stores the canonical discovery URL so it stays
tenant-agnostic and re-usable across users.
Apologies for previously concluding the catalog URL pattern was correct
based on raw curl tests — the SDK's full handshake reaches a deeper
code path that surfaces the real tenant requirement.
Replace the hand-rolled browser-oauth.ts and device-code-oauth.ts with @azure/msal-node's PublicClientApplication. MSAL handles PKCE, token caching, refresh, and redirect URIs correctly out of the box. Browser flow now uses http://localhost (ephemeral port, no /callback path) which matches the redirect URI registered on MSAL-compatible Entra apps (FOCI / VS Code / az CLI). This fixes AADSTS50011 redirect URI mismatch when using the VS Code app ID. Device-code flow uses acquireTokenByDeviceCode — prints verification URL + user code to stderr, no redirect URI needed. Changes: - Add @azure/msal-node dependency - New src/agent/mcp/auth/msal-oauth.ts (MSAL provider + file cache) - Delete browser-oauth.ts and device-code-oauth.ts - MCPOAuthConfig: flow is required (browser|device-code), callbackPort replaced with optional redirectUri - client-manager: single connectWithMsal replaces two methods - Scripts/Justfile: FLOW arg required, CALLBACK_PORT removed - Tests: MSAL provider tests, flow validation tests Tested: just check passes (2198/2198 tests, lint clean)
- Use ea9ffc3e-.../.default (Agent 365 resource) instead of per-server scopes. Per-server scopes aren't fully qualified so MSAL falls back to Graph, which breaks with FOCI apps. Matches a365cli behaviour. - Always print the auth URL to stderr so users can copy/paste when xdg-open isn't available (headless distros, SSH, no browser setup). - Remove callbackPort from catalog JSON. Tested: browser flow with VS Code FOCI app — works end-to-end.
…igure hint
- m365-refresh-servers: read MSAL .msal.json cache format only, drop
legacy {savedAt, tokens} format (deleted provider wrote that)
- slash-commands: remove hardcoded allowedContentTypes reconfigure
suggestion from /plugin enable — was a fake example that doesn't
exist on most plugins
Enable the LLM to discover and connect MCP servers autonomously:
1. manage_mcp('connect') now works end-to-end:
- Pre-approved servers connect silently
- Unapproved + interactive TTY → prompts user for approval
- Unapproved + no TTY → refuses with clear error
- Mirrors the approval flow from /mcp enable
2. MCP gateway auto-enables on startup when servers are configured
(the plugin is a boolean sentinel — zero risk, no audit needed)
3. list_mcp_servers() ungated — works before gateway is enabled
so the LLM can discover servers without user running /plugin enable
4. System prompt: dynamic MCP section — concise hint when servers
are configured, tells LLM to use tools for discovery instead of
dumping a static docs block
5. Generic MCP skill (skills/mcp-services/SKILL.md) — teaches the
full workflow: discover → connect → get schemas → import → call
6. mcp-setup-m365 pre-approves configured servers in the approval
store so the LLM can connect them without interactive prompts
Target flow: user asks 'What is in Teams?' → LLM discovers servers →
connects work-iq-teams (pre-approved) → calls tool → returns data.
Tested: just check passes (2198/2198 tests, lint clean)
M365 catalog has 21 servers alone — adding GitHub/filesystem pushed over the limit. Config parsing failed silently, gateway didn't auto- enable, LLM had no MCP awareness.
manage_mcp('connect') now checks if MSAL can acquire a token silently
before attempting connection. If interactive auth (browser/device-code)
would be needed, it returns immediately with an error telling the LLM
to direct the user to /mcp enable <name> instead of hanging on a
browser window inside a tool call.
Once the user has authenticated once via /mcp enable, subsequent
manage_mcp('connect') calls use the cached token and connect instantly.
Added canAcquireSilently() to msal-oauth.ts — tries acquireTokenSilent
and returns a boolean without triggering interactive flows.
The MCP gateway plugin source hash changes on every npm install (rebuild), invalidating the old approval. syncPluginsToSandbox then refuses to load it. Fix: when auto-enabling the gateway, set a synthetic audit result and approve with the current content hash. The plugin is a boolean sentinel (returns true from a single function) so auto-approving is zero risk.
Three fixes: 1. /mcp enable refuses OAuth servers in --auto-approve/yolo mode when no cached silent token exists. Prevents opening a browser that nobody is watching in CI/pipeline scenarios. 2. Add /mcp enable to ACTIONABLE_COMMAND_PREFIXES so the LLM's suggestion to run '/mcp enable work-iq-teams' gets picked up by extractSuggestedCommands and offered as a one-click [Y/n] prompt. This closes the 'user has to manually type the command' gap. 3. Fix 'too many servers' test to match bumped MAX_MCP_SERVERS (50). Auth safety model: - manage_mcp tool call: canAcquireSilently check → refuses if interactive needed - /mcp enable (interactive): browser opens, user authenticates → fine - /mcp enable (yolo): canAcquireSilently check → refuses with clear message - Once authenticated: cached tokens → everything works silently
Write-safety gate: - Capture tool annotations (readOnlyHint, destructiveHint, etc.) from MCP listTools() into MCPToolSchema - Intercept non-read-only tool calls in plugin-adapter before execution - Interactive TTY: prompt user 'Allow? [y/n]' with tool name + args - --auto-approve: allow all operations (yolo is yolo) - No TTY + no auto-approve: refuse with clear error - Gate is transparent to the LLM — it sees results or error objects Docs (MCP.md): - Rewrite HTTP/OAuth section: MSAL, flow field, redirect URI, scopes - Rewrite M365 section: VS Code FOCI app, mcp-setup-m365 recipes, auth flows (browser/device-code), scope (.default), pre-approval - Add write-safety gate section with decision matrix - Update quick start: gateway auto-enables, LLM-driven discovery - Update architecture diagram with gate - Fix stale references (callbackPort, hand-rolled PKCE, old recipe names) - Bump max servers to 50 Tested: just check passes (2198/2198 tests, lint clean)
1. Write-safety gate now remembers approved tools for the session. First call to SearchMessages prompts [y/n], subsequent calls to the same tool skip the prompt. Avoids prompting on every read when servers don't provide readOnlyHint annotations. 2. Pre-approve MCP gateway at discovery time (before any sync), not just at auto-enable time. Fixes the 'REFUSING to load' error on startup when the plugin hash changed since last session.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR expands HyperAgent’s MCP integration to support HTTP-based MCP servers (notably Microsoft 365 / Agent 365 endpoints) with OAuth via MSAL, adds safer MCP tool execution behavior, and improves setup/build tooling to make MCP configuration and generated artifacts more robust.
Changes:
- Added HTTP transport support for MCP servers, including multiple auth modes (OAuth/MSAL, workload identity, client credentials), plus session and retry helpers.
- Introduced a write-safety gate for MCP tool calls and improved MCP server discovery/UX (auto-enable gateway when configured, richer display strings).
- Added cross-platform scripts and updated docs/Just recipes for M365 MCP setup, plus build/clean improvements and expanded test coverage.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/mcp.test.ts | Extends MCP config/hash/type-guard/client-manager/auth provider tests for HTTP + OAuth scenarios. |
| tests/mcp-retry-fetch.test.ts | Adds unit tests for the new retrying fetch wrapper used by HTTP transports. |
| tests/mcp-extract-embedded-json.test.ts | Adds tests for extracting structured JSON from Agent 365 “text-wrapped JSON” response patterns. |
| src/agent/system-message.ts | Makes MCP guidance conditional in the system prompt based on whether MCP is configured. |
| src/agent/slash-commands.ts | Improves MCP UX output (command vs URL display) and adds OAuth headless/auto-approve preflight handling. |
| src/agent/mcp/types.ts | Introduces HTTP MCP server config types, auth union, tool annotations, display helpers, and raises server limit to 50. |
| src/agent/mcp/session-cache.ts | Adds persisted Streamable HTTP session-id cache to resume MCP sessions across restarts. |
| src/agent/mcp/retry-fetch.ts | Adds retry/backoff middleware for transient HTTP MCP gateway failures (429/502/503/504). |
| src/agent/mcp/plugin-adapter.ts | Adds optional write-safety gate hook to intercept non-read-only MCP tool executions. |
| src/agent/mcp/config.ts | Extends MCP config parsing/validation to support HTTP transport + auth; updates config hashing. |
| src/agent/mcp/client-manager.ts | Adds Streamable HTTP transport support, MSAL OAuth connect path, session caching, and retry fetch integration. |
| src/agent/mcp/auth/token-cache.ts | Adds on-disk OAuth token cache utilities under ~/.hyperagent/mcp-tokens/. |
| src/agent/mcp/auth/msal-oauth.ts | Implements MSAL-backed OAuthClientProvider integration for MCP Streamable HTTP transport. |
| src/agent/index.ts | Auto-approves/enables MCP gateway when servers are configured; wires write-safety gate; enhances MCP tools’ behavior/output. |
| src/agent/command-suggestions.ts | Allows /mcp enable to be detected as an actionable suggested command. |
| skills/mcp-services/SKILL.md | Adds an MCP workflow skill for connecting/inspecting/using MCP services (notably M365). |
| scripts/setup-m365-app.ts | Adds cross-platform Entra app registration bootstrapper for M365 MCP usage via az. |
| scripts/mcp-add-http.ts | Adds cross-platform helper to write HTTP MCP server entries into ~/.hyperagent/config.json. |
| scripts/m365-show.ts | Adds helper to print saved M365 app registration state from ~/.hyperagent/m365.json. |
| scripts/m365-setup.ts | Adds catalog-driven M365 MCP config writer and pre-approver. |
| scripts/m365-refresh-servers.ts | Adds catalog refresh tool to update scripts/m365-mcp-servers.json from the live Agent 365 discovery endpoint. |
| scripts/m365-mcp-servers.json | Adds a curated M365/Agent365 MCP server catalog (url + scope per service). |
| scripts/build-modules.js | Adjusts build ordering to compile TS before regenerating ha-modules.d.ts to avoid partial/broken fresh builds. |
| package.json | Adds @azure/msal-node dependency for OAuth authentication. |
| package-lock.json | Locks MSAL and transitive dependencies. |
| docs/MCP.md | Updates MCP documentation for HTTP/OAuth, M365 setup, auto-enable behavior, write-safety gate, and troubleshooting. |
| Justfile | Adds robust clean recipe; improves MCP config display; adds M365/HTTP MCP setup recipes. |
1. Header validation: verify all header values are strings with non-empty keys, not just that headers is an object. 2. Config hash: include flow, tenantId, scopes, redirectUri, clientSecretEnv, and header keys in the SHA-256 hash. Approval is now invalidated on any meaningful HTTP config change. Updated m365-setup.ts hash computation to match. 3. Require scopes for OAuth: resolveScopes() throws if no scopes configured instead of silently using only offline_access. Config validator now rejects missing/empty scopes for OAuth. 4. Non-interactive connect: use canAcquireSilently() instead of acquireMsalToken() in non-interactive mode. Never falls back to interactive auth in headless/no-TTY scenarios. All test fixtures updated for required scopes. 2198/2198 pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces significant improvements to the MCP (Model Context Protocol) documentation, setup scripts, and build tooling, with a focus on supporting HTTP-based MCP servers (notably Microsoft 365/Agent 365 per-service endpoints) with OAuth authentication. It also enhances the safety and usability of MCP tool execution, adds a robust clean script for build artifacts, and clarifies the plugin's auto-enabling behavior. The most important changes are grouped below.
MCP HTTP/OAuth Support and Microsoft 365 Enhancements:
mcp-add-http,mcp-setup-m365, etc.) in theJustfileto configure HTTP MCP servers with OAuth, including support for Microsoft 365/Agent 365 per-service endpoints, custom Entra app registrations, and catalog refresh.docs/MCP.md) with detailed instructions for HTTP MCP server setup, OAuth flows (browser/device-code), token caching, and Microsoft 365-specific configuration, including examples and troubleshooting.@azure/msal-nodeas a dependency inpackage.jsonfor OAuth 2.0 authentication support.Plugin Usability and Safety Improvements:
/plugin enable mcpcommands. Documentation updated accordingly.readOnlyHintannotations now prompt the user before executing potentially destructive operations, with clear behavior for interactive, auto-approve, and headless modes.Build and Clean Tooling:
cleanrecipe in theJustfile(for both Unix and Windows) to remove build artifacts, stale generated files, and restore committed type definitions, preventing issues after failed builds.scripts/build-modules.jsto ensure TypeScript compilation occurs before regenerating type definitions, preventing partial or broken builds on fresh checkouts.User Interface Improvements:
mcp-show-configscript.These changes collectively make MCP server setup, management, and usage more robust, user-friendly, and secure, especially for Microsoft 365/Agent 365 integrations.