Skip to content

Feat/m365 mcp#83

Merged
simongdavies merged 15 commits intohyperlight-dev:mainfrom
simongdavies:feat/m365-mcp
Apr 28, 2026
Merged

Feat/m365 mcp#83
simongdavies merged 15 commits intohyperlight-dev:mainfrom
simongdavies:feat/m365-mcp

Conversation

@simongdavies
Copy link
Copy Markdown
Member

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:

  • Added setup recipes (mcp-add-http, mcp-setup-m365, etc.) in the Justfile to configure HTTP MCP servers with OAuth, including support for Microsoft 365/Agent 365 per-service endpoints, custom Entra app registrations, and catalog refresh.
  • Updated documentation (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.
  • Added @azure/msal-node as a dependency in package.json for OAuth 2.0 authentication support.

Plugin Usability and Safety Improvements:

  • The MCP gateway plugin now auto-enables when servers are configured, removing the need for manual /plugin enable mcp commands. Documentation updated accordingly.
  • Introduced a write-safety gate for MCP tools: tools without readOnlyHint annotations now prompt the user before executing potentially destructive operations, with clear behavior for interactive, auto-approve, and headless modes.
  • Increased the maximum number of configured servers from 20 to 50.
  • Clarified and improved the LLM's MCP discovery and connection process, including auto-approval and OAuth browser prompts.

Build and Clean Tooling:

  • Added a robust clean recipe in the Justfile (for both Unix and Windows) to remove build artifacts, stale generated files, and restore committed type definitions, preventing issues after failed builds.
  • Improved the build process in scripts/build-modules.js to ensure TypeScript compilation occurs before regenerating type definitions, preventing partial or broken builds on fresh checkouts.

User Interface Improvements:

  • Enhanced the MCP server config display to show HTTP server URLs and authentication methods in the mcp-show-config script.

These changes collectively make MCP server setup, management, and usage more robust, user-friendly, and secure, especially for Microsoft 365/Agent 365 integrations.

- 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.
Copilot AI review requested due to automatic review settings April 27, 2026 21:22
@simongdavies simongdavies added the enhancement New feature or request label Apr 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/agent/mcp/config.ts
Comment thread src/agent/mcp/config.ts
Comment thread src/agent/mcp/auth/msal-oauth.ts
Comment thread src/agent/mcp/client-manager.ts Outdated
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.
@simongdavies simongdavies merged commit 616b4d1 into hyperlight-dev:main Apr 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants