Skip to content

MCP Server addition and modification of SimpleChat API to accomodate …#722

Open
gregunger-microsoft wants to merge 1 commit intoDevelopmentfrom
feature/gunger-mcp-server
Open

MCP Server addition and modification of SimpleChat API to accomodate …#722
gregunger-microsoft wants to merge 1 commit intoDevelopmentfrom
feature/gunger-mcp-server

Conversation

@gregunger-microsoft
Copy link
Collaborator

Added MCP Server code with PRM auth handshake and very minimal SimpleChat API changes to accomodate auth/session.

tried to assign to Paul/Nathaniel but it is not working.
/assign @paullizer
/assign @nadoylemsft

Copy link
Collaborator

Choose a reason for hiding this comment

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

Overall, reads like a great start and I think the features exposed are awesome. It needs some enterprise TLC (scaling via redis for session cache, appinsights/LAW logging support, etc), but a great start none the less.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unclear on where you see redis coming into play here with regard to scaling? (on the MCP server side)

SimpleChat handles session. The MCP Server only has a session created during the auth handshake and then subsequently sends a Session ID as a cookie with requests to the SimpleChat API.

The MCP Server itself can easily scale out if needs be via horizontal scaling to accommodate concurrent user load.

But before we get into refactoring for scaling, we should harden any patterns used by integration components, including the MCP Server as a first pass before we start coding for scaling.

Again, if there is some way you could be more prescriptive in exactly what you had in mind, I'm happy to accommodate.

Same thing with logging here. If there is some logging pattern you want implement, let's discuss.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought I saw where the MCP server is maintaining its own session state, if I was mistaken, then scaling is already built in by being stateless.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason this would not run in a distroless (mcr.microsoft.com/azurelinux/distroless/python:3.12) multi-step build like the core app?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So no issues if distroless is a requirement. But at this point in the development, it is premature for the MCP Server itself and for non-production use-cases. We could easily have multiple docker files to accomodate distroless and not distroless.

Using a distroless container image means your container includes only your application and its runtime — no shell, no package manager, no debugging tools.

That sounds ideal, especially prematurely when adding new functionality/components to a system that may require higher visibility/observability in a given system.

Here are the main reasons why using a distroless image can be a bad idea:
#1 Debugging Becomes Painful
Distroless images don’t include:
bash / sh
curl
ps
netstat
apt / yum

  • i.e. Most standard Linux tools

If something goes wrong in production, you can’t exec into the container and inspect it. Because there is no shell.

This makes incident response slower and more complex. You need:

  • Sidecar debug containers
  • Ephemeral debug images
  • Rebuilds just to inspect behavior

That’s fine for mature software. Not so great for fast-moving agile software development.

#2 Local Development Becomes Friction-Filled
Distroless is great for production minimalism.
It’s terrible for:

  • Experimentation
  • Rapid troubleshooting
  • Investigating weird runtime edge cases

Developers often end up maintaining:

  • One Dockerfile for dev
  • One for prod

Now you have divergence risk.

#3 Observability Tooling Limitations
Many debugging or monitoring tools assume basic OS utilities exist.
If you rely on:

  • Runtime profilers
  • On-the-fly tracing
  • Crash dump analysis
  • Temporary diagnostics
    You may discover the container simply doesn’t support what you need.

#4 Operational Complexity Increases
Distroless pushes complexity outward.
Instead of “Let’s exec in and look.”
You now need:

  • Debug sidecars
  • Separate debug images
  • Stronger logging discipline
  • More complete telemetry
    If your observability is weak, distroless will expose it brutally.

#5 Compatibility Surprises
Some apps assume:

  • Timezone data exists
  • CA certificates are present
  • Locale data is available
  • /bin/sh exists for subprocesses
    Distroless images may omit or minimally include these.
    You discover issues at runtime.

#6 Security Gains May Be Marginal
Distroless is often sold as “more secure.”
But:

  • The real attack surface is usually your application.
  • Most container exploits rely on runtime vulnerabilities, not bash.
  • A properly hardened minimal distro (e.g., Alpine or slim Debian) may provide nearly identical risk reduction.
    Security benefit is real — but sometimes overstated.

#7 Not Ideal for Dynamic Environments
If your application:

  • Installs plugins dynamically
  • Runs scripts
  • Uses subprocess-heavy workflows
  • Executes user-provided commands
    Distroless can break those assumptions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It will be a requirement for production (a ton of customer have requested it because of the amount of CVEs that are eliminated), but your points are valid for rapid iteration/development. That said, for rapid iteration, you should also be able to run it locally/dev container that has obserability/tools. We also prefer to rely on high and strong logging discipline.

Comment on lines +8 to +11
def register_route_external_prm(app):
@app.route('/.well-known/oauth-protected-resource', methods=['GET'])
@swagger_route(security=get_auth_security())
def get_prm_metadata():
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this route need to be unauthenticated (pretty sure it does)? Security risks associated with exposing the app id and tenantId to a non-authenticated endpoint? We likely will need to document this, and we need to add to code as comments that this route is explicitly unprotected by authentication (this might actually be our first one).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No — a PRM authentication file does not need to be unauthenticated for MCP server.

In a typical MCP (Model Context Protocol) server–client handshake, the sequence looks like:
1 Client connects to server
2 Client presents credentials (token, key, cert, etc.)
3 Server validates credentials
4 Secure session is established
The authentication material (like a PRM file containing keys or metadata) is used during validation, not before it.
So the file does not need to be publicly accessible or unauthenticated.

Copy link
Collaborator

@Bionic711 Bionic711 left a comment

Choose a reason for hiding this comment

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

Not prod ready, but a fantastic start. We need the option to disable and enabled the mcp server from admin options, with a default to disable which includes a note that it requires a restart of the app to take effect. Scaling via Redis (at minimal for session cache and I imagine many customers will implement in app service instead of ACA as that is how most of them run it). Logging into AppInsights with OTEL.

Logging and scaling we could approve without. However, the admin settings via opt-in is required before we can get this into development. We should have a modal on the configuration page that guides the admin through the general steps to deploy the external app (platform agnostic) and, if we leave the metadata page unauthenticated, a pop-up that the admin needs to accept (and is then logged to the activity log with which admin did it) that they accepted that the tenantid and appid are exposed on an unauthenticated endpoint.

Copy link
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

Adds an external MCP (Model Context Protocol) server integration and extends SimpleChat’s auth surface to support PRM-based OAuth discovery and bearer-token-to-session bridging for API-to-API SSO.

Changes:

  • Added a minimal FastMCP server with PRM auth shim, device-code login option, and SimpleChat API tool wrappers.
  • Added new SimpleChat endpoints for PRM metadata (/.well-known/oauth-protected-resource) and external session creation (POST /external/login).
  • Updated existing auth flow to optionally create a server-side session from /getATokenApi.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
application/single_app/route_frontend_authentication.py Adds helpers and extends /getATokenApi to optionally create a session and accept a local redirect URI.
application/single_app/route_external_prm.py Introduces PRM discovery endpoint for OAuth-protected resource metadata.
application/single_app/route_external_authentication.py Adds /external/login to create a SimpleChat session from a validated Entra bearer token.
application/single_app/functions_authentication.py Expands accesstoken_required authorization logic and exposes claims via flask.g.
application/single_app/app.py Registers new external auth + PRM routes.
application/external_apps/mcp/server_minimal.py New MCP server implementation, PRM auth shim, session caching, and SimpleChat tool calls.
application/external_apps/mcp/run_mcp_server.ps1 Local runner script for the MCP server.
application/external_apps/mcp/requirements.txt MCP server Python dependencies.
application/external_apps/mcp/prm_metadata.json PRM metadata JSON consumed by the MCP server shim.
application/external_apps/mcp/example.env Example environment config for MCP server deployment/runs.
application/external_apps/mcp/deploy_mcp_containerapp.ps1 Azure Container Apps build/deploy script for the MCP server.
application/external_apps/mcp/README.md MCP server usage and configuration documentation.
application/external_apps/mcp/Dockerfile Container image build for the MCP server.
application/external_apps/mcp/.dockerignore Docker ignore rules for the MCP server build context.
.gitignore Adds ignores for MCP .env.azure and a user-specific local folder.

Comment on lines +467 to +468
if not is_external_api_authorized(data):
return jsonify({"message": "Forbidden: ExternalApi, User, or Admin role/scope required"}), 403
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

accesstoken_required now authorizes tokens with User/Admin role or scope, not just ExternalApi. This unintentionally grants access to all /external/public_documents/* endpoints (which rely on caller-supplied user_id / active_workspace_id) to regular user tokens, enabling cross-user actions. Consider keeping accesstoken_required restricted to ExternalApi (app-only) and introducing a separate decorator (or parameter) for /external/login that validates the bearer token but does not broaden authorization for other external routes; also prefer deriving user_id from token claims when possible.

Copilot uses AI. Check for mistakes.
Comment on lines +504 to +508
scope_values = [scope for scope in scopes_raw.split() if scope]
for scope in scope_values:
scope_name = scope.split("/")[-1]
if scope_name in allowed_roles:
return True
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

is_external_api_authorized treats scope values like "api://.../User" as authorization for external APIs by mapping the last path segment to a role name. This makes it easy to accidentally authorize unintended scopes that merely end with User/Admin/ExternalApi. Tighten this to an explicit allowlist of full scope strings (or check aud + exact scp values) to avoid false-positive authorization.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +12


def register_route_external_prm(app):
@app.route('/.well-known/oauth-protected-resource', methods=['GET'])
@swagger_route(security=get_auth_security())
def get_prm_metadata():
resource = request.host_url.rstrip('/')
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

resource = request.host_url will often produce the wrong scheme (http://...) behind Azure ingress / reverse proxies unless ProxyFix (or equivalent) is configured. Since PRM metadata is used by clients for subsequent auth, consider deriving scheme from X-Forwarded-Proto (and host from X-Forwarded-Host when present) to ensure resource is correct in production.

Suggested change
def register_route_external_prm(app):
@app.route('/.well-known/oauth-protected-resource', methods=['GET'])
@swagger_route(security=get_auth_security())
def get_prm_metadata():
resource = request.host_url.rstrip('/')
from flask import request, jsonify
def register_route_external_prm(app):
@app.route('/.well-known/oauth-protected-resource', methods=['GET'])
@swagger_route(security=get_auth_security())
def get_prm_metadata():
# Derive external-facing resource URL in a proxy-aware way
scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
host = (
request.headers.get('X-Forwarded-Host')
or request.headers.get('Host')
or request.host
)
resource = f"{scheme}://{host}".rstrip('/')

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +43
def register_route_external_authentication(app):
@app.route('/external/login', methods=['POST'])
@swagger_route(security=get_auth_security())
@accesstoken_required
def external_login():
"""
Creates a server-side session using a validated Entra bearer token.
Returns session details for external clients (e.g., MCP servers).
"""
claims = getattr(g, "user_claims", None)
if not isinstance(claims, dict):
return jsonify({"error": "Unauthorized", "message": "No user claims available"}), 401

session["user"] = claims

session_id = getattr(session, "sid", None) or session.get("session_id") or session.get("_id")
if not session_id:
session_id = str(uuid4())
session["session_id"] = session_id

response_payload = {
"session_created": True,
"session_id": session_id,
"user": {
"userId": claims.get("oid") or claims.get("sub"),
"displayName": claims.get("name"),
"email": claims.get("preferred_username") or claims.get("email")
},
"claims": claims
}

debug_print(f"External login session created for user {response_payload['user'].get('userId')}")
return jsonify(response_payload), 200
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

New auth/session behavior is being added (/external/login, PRM endpoint, and broadened bearer-token auth). There are extensive functional_tests/ in this repo, but this PR doesn’t add coverage for the new external login handshake. Please add a functional test that (1) calls /.well-known/oauth-protected-resource, and (2) calls /external/login with a valid bearer token and verifies a session cookie/session id is established.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +645 to +649
# ------------------- External Authentication Routes ---
register_route_external_authentication(app)

# ------------------- PRM Metadata Routes --------------
register_route_external_prm(app)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This PR introduces new externally-consumable endpoints and an MCP integration, but config.py’s VERSION is not bumped. Per the repo’s versioning pattern (third segment only), please increment the patch version in application/single_app/config.py so the change is traceable in deployments.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +190
# This route is for API calls that need a token, not the web app login flow. This does not kick off a session.
@app.route('/getATokenApi') # This is your redirect URI path
@swagger_route(security=get_auth_security())
def authorized_api():
create_session = _parse_bool(request.args.get('create_session'), default=False)
request_redirect_uri = request.args.get('redirect_uri')
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The comment above authorized_api says this route “does not kick off a session”, but the new create_session query param explicitly creates a session and persists token cache. Update the comment so future callers don’t rely on outdated behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +96
# Session cache: bearer_token -> requests.Session
_SESSION_CACHE: Dict[str, requests.Session] = {}
_SESSION_LOCK = threading.Lock()

# Cache the /external/login payload (contains user + claims) per bearer token.
_LOGIN_PAYLOAD_CACHE: Dict[str, Dict[str, Any]] = {}

# Cache bearer token per MCP streamable-http session id. This lets the server reuse
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

_SESSION_CACHE / _LOGIN_PAYLOAD_CACHE are keyed by the raw bearer token and never evicted. In a long-running server this can grow without bound (new token => new entry) and also keeps tokens resident in memory longer than necessary. Consider keying by a hash of the token and adding TTL/LRU eviction (or clearing caches on token expiry) to avoid unbounded memory growth.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +25
[string]$SubscriptionId = "56013a89-2bdc-403e-9f7f-17da3c9d8ab4",

[Parameter(Mandatory = $false)]
[string]$ResourceGroup = "aaronba-simplechat-rg",

[Parameter(Mandatory = $false)]
[string]$Location = "",

[Parameter(Mandatory = $false)]
[string]$ContainerAppName = "gunger-simplechat-mcp",

[Parameter(Mandatory = $false)]
[string]$EnvironmentName = "aaronba-simplechat-v2-env",

[Parameter(Mandatory = $false)]
[string]$AcrName = "aaronbasimplechatacr",

[Parameter(Mandatory = $false)]
[string]$ImageName = "gunger-simplechat-mcp",
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The default parameter values (subscription ID, resource group, ACR name, environment name, etc.) look like personal/real Azure resources. Keeping real defaults in-repo makes accidental deployments more likely and can leak internal resource naming. Consider making these parameters required (no defaults) or using clearly placeholder defaults.

Suggested change
[string]$SubscriptionId = "56013a89-2bdc-403e-9f7f-17da3c9d8ab4",
[Parameter(Mandatory = $false)]
[string]$ResourceGroup = "aaronba-simplechat-rg",
[Parameter(Mandatory = $false)]
[string]$Location = "",
[Parameter(Mandatory = $false)]
[string]$ContainerAppName = "gunger-simplechat-mcp",
[Parameter(Mandatory = $false)]
[string]$EnvironmentName = "aaronba-simplechat-v2-env",
[Parameter(Mandatory = $false)]
[string]$AcrName = "aaronbasimplechatacr",
[Parameter(Mandatory = $false)]
[string]$ImageName = "gunger-simplechat-mcp",
[string]$SubscriptionId = "REPLACE_WITH_SUBSCRIPTION_ID",
[Parameter(Mandatory = $false)]
[string]$ResourceGroup = "REPLACE_WITH_RESOURCE_GROUP",
[Parameter(Mandatory = $false)]
[string]$Location = "",
[Parameter(Mandatory = $false)]
[string]$ContainerAppName = "REPLACE_WITH_CONTAINERAPP_NAME",
[Parameter(Mandatory = $false)]
[string]$EnvironmentName = "REPLACE_WITH_ENVIRONMENT_NAME",
[Parameter(Mandatory = $false)]
[string]$AcrName = "REPLACE_WITH_ACR_NAME",
[Parameter(Mandatory = $false)]
[string]$ImageName = "REPLACE_WITH_IMAGE_NAME",

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +9
"https://login.microsoftonline.com/7d887458-fb0d-40bf-adb3-084d875f65db/v2.0"
],
"scopes_supported": [
"api://0b8c00b9-4dcd-4959-83be-7a0521ce54ce/.default"
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This PRM metadata file contains what appear to be real tenant/client IDs. Since this repo is shared, consider replacing these with placeholders (or moving to an explicit example file) to avoid coupling the repo to a specific Entra tenant/app registration.

Suggested change
"https://login.microsoftonline.com/7d887458-fb0d-40bf-adb3-084d875f65db/v2.0"
],
"scopes_supported": [
"api://0b8c00b9-4dcd-4959-83be-7a0521ce54ce/.default"
"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
],
"scopes_supported": [
"api://{APPLICATION_CLIENT_ID}/.default"

Copilot uses AI. Check for mistakes.
# SimpleChat MCP Server (FastMCP)

This MCP server provides **14 tools** for interacting with SimpleChat via the Model Context Protocol (Streamable HTTP transport).

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This README documents the MCP server, but the repo’s main feature docs live under docs/explanation/features/<version>/.... Consider adding a corresponding feature doc there (and linking to this README) so the new external integration is discoverable alongside other SimpleChat features.

Suggested change
For a high-level overview of this integration alongside other SimpleChat features, see the MCP feature documentation under `docs/explanation/features/<version>/MCP_SERVER_INTEGRATION.md`.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments