MCP Server
Declare an MCP resource in SqlOS and host Microsoft's MCP SDK on that path.
SqlOS is the auth server. A Model Context Protocol endpoint is a protected resource: an audience, an RFC 9728 document, and (for portable clients) CIMD plus resource indicators. The transport is Microsoft's MCP C# SDK. SqlOS does not take an MCP SDK dependency and does not map the server for you.
dotnet add package SqlOS
dotnet add package ModelContextProtocol.AspNetCoreusing ModelContextProtocol.AspNetCore;
using SqlOS.AuthServer.Authentication;
builder.AddSqlOS<AppDbContext>(
db => db.UseSqlServer(connectionString),
options => options.UseSingleApplication("PetalPal", app =>
{
app.Origin = "https://petalpal.example.com";
app.Api = "/api";
app.Mcp = "/mcp";
}));
builder.Services.AddHttpContextAccessor();
builder.Services.AddMcpServer()
.WithHttpTransport(transport => transport.SessionMode = HttpServerSessionMode.Stateless)
.WithTools<GardenMcpTools>();
var app = builder.Build();
app.MapMcp("/mcp").RequireAuthorization(SqlOSJwtDefaults.McpPolicy);
app.Run();app.Mcp = "/mcp" is settings. From that assignment SqlOS:
| Concern | Behavior |
|---|---|
| Audience | Scheme/policy SqlOS.Mcp expects a bearer token whose aud is exactly {Origin}/mcp. A token minted for /api is rejected, and vice versa. |
| Challenge | Unauthenticated or wrong-audience requests receive 401 with WWW-Authenticate: Bearer realm="PetalPal MCP", resource_metadata="{Origin}/.well-known/oauth-protected-resource/mcp". |
| Discovery | GET /.well-known/oauth-protected-resource/mcp returns the RFC 9728 document with resource, authorization_servers, scopes_supported, and bearer_methods_supported. |
| Client onboarding | Client ID metadata documents and resource indicators are enabled, so portable MCP clients register with an HTTPS metadata URL and request resource={Origin}/mcp. Dynamic client registration stays off unless you enable it. |
The host still writes AddMcpServer / MapMcp and locks the endpoint with RequireAuthorization("SqlOS.Mcp"). SqlOS does not mint a special MCP token type, change the OpenID Provider, or authorize garden (or notes) data. Tools call your API / FGA.
Portable clients that register a portless loopback redirect (for example http://127.0.0.1/callback/<id>) and bind an ephemeral port at sign-in are accepted per RFC 8252 §7.3, as described in OAuth for MCP clients.
Tools are ordinary MCP SDK tools. Read the connecting user with GetSqlOSValidatedToken() on the current HttpContext.
using System.ComponentModel;
using ModelContextProtocol.Server;
using SqlOS.AuthServer.Extensions;
using SqlOS.Fga.Interfaces;
public sealed class GardenMcpTools
{
[McpServerTool(Name = "list_gardens"), Description("Lists the gardens the connecting user can read.")]
public static async Task<IReadOnlyList<GardenSummary>> ListGardens(
IHttpContextAccessor http,
GardenService gardens,
CancellationToken ct)
{
var userId = http.HttpContext?.GetSqlOSValidatedToken()?.UserId
?? throw new InvalidOperationException("A user token is required.");
return await gardens.ListReadableAsync(userId, ct);
}
}Resolve the acting identity from the validated token, never from tool arguments, and apply FGA or application policy per tool: audience validation proves the token was minted for this MCP server, not that the user may read a given garden.
Api and Mcp are separate audiences under the same origin. The first-party client receives {Origin}/api tokens by default; MCP clients request {Origin}/mcp through the resource parameter. Both surfaces share the auth server, sessions, users, organizations, and FGA model. Lock API routes with RequireAuthorization(). Lock MapMcp with RequireAuthorization("SqlOS.Mcp"). See how tokens are validated.
Surface paths are validated at startup: absolute, non-root, distinct and non-nested, and outside /.well-known, the auth base path, and DashboardBasePath.
SqlOS.Mcp was a convenience wrapper around the SDK (AddMcpServer, stateless Streamable HTTP, MapMcp, a user-context adapter, and a tool-call audit filter). Drop that package. Set app.Mcp = "/mcp", reference ModelContextProtocol.AspNetCore, and map the server in the host. Tools that injected ISqlOSMcpUserContext should call GetSqlOSValidatedToken() instead. Keep any tool-call audit filter in the host if you still want those events.
curl -i https://petalpal.example.com/.well-known/oauth-protected-resource/mcp
curl -i -X POST https://petalpal.example.com/mcp -H 'content-type: application/json' -d '{}'The first request returns the metadata document; the second returns 401 with the Bearer challenge above. After a client completes authorization with resource=https://petalpal.example.com/mcp, tools/list and tools/call succeed.