Skip to content

Commit df45131

Browse files
committed
feat: v3.17.0 — MCP non-applicable-feature warning + AOT fix
- Warn at endpoint-creation time when a routine annotated 'mcp' also uses a feature with no MCP-tool equivalent (login/logout/basic auth/upload/SSE). The tool is still exposed; the warning flags it likely won't behave over JSON-RPC. Tested via a log-capturing fixture + isolated mcp_warn schema. - Fix IL2026/IL3050 in tools/call: content.Add(new JsonObject{...}) bound the non-AOT-safe generic JsonArray.Add<T>; cast to JsonNode? to bind the non-generic overload. Verified clean via dotnet publish -p:PublishAot=true (remaining publish warnings are pre-existing third-party).
1 parent 2ec90ff commit df45131

5 files changed

Lines changed: 118 additions & 2 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using NpgsqlRestTests.Setup;
2+
3+
namespace NpgsqlRestTests;
4+
5+
public static partial class Database
6+
{
7+
// Isolated schema for the non-applicable-feature warning test. `mcp_warn` is excluded from every
8+
// other fixture (they include only "mcp"/"public"), so this routine never pollutes other catalogs.
9+
public static void McpFeatureWarnTests()
10+
{
11+
script.Append(@"
12+
create schema if not exists mcp_warn;
13+
14+
-- @mcp on a login endpoint: login is an auth flow that does not translate to an MCP tool call.
15+
-- (login routines must return a named result set, not a scalar/void.)
16+
create function mcp_warn.tool_login() returns table(status int, name_identifier text)
17+
language sql as 'select 200, ''42''';
18+
comment on function mcp_warn.tool_login() is '
19+
HTTP POST
20+
login
21+
@mcp Sign in.';
22+
");
23+
}
24+
}
25+
26+
[Collection("McpFeatureWarnFixture")]
27+
public class McpFeatureWarnTests(McpFeatureWarnTestFixture test)
28+
{
29+
[Fact]
30+
public void Mcp_annotation_on_a_non_applicable_feature_logs_a_warning()
31+
{
32+
var warning = test.StartupLogs.FirstOrDefault(l =>
33+
l.Message.Contains("does not apply to MCP tools") && l.Message.Contains("login"));
34+
warning.Should().NotBeNull("the plugin should warn when `@mcp` is placed on a login routine");
35+
warning!.Message.Should().Contain("mcp_warn.tool_login");
36+
}
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
using NpgsqlRest.Mcp;
5+
6+
namespace NpgsqlRestTests.Setup;
7+
8+
[CollectionDefinition("McpFeatureWarnFixture")]
9+
public class McpFeatureWarnFixtureCollection : ICollectionFixture<McpFeatureWarnTestFixture> { }
10+
11+
/// <summary>
12+
/// Fixture for the MCP non-applicable-feature warning. The isolated <c>mcp_warn</c> schema holds a
13+
/// routine that is annotated <c>@mcp</c> but is also a <c>login</c> endpoint — a feature that does not
14+
/// translate to an MCP tool call. The plugin should log a warning at endpoint-creation time. Logs are
15+
/// captured so the test can assert the warning fired.
16+
/// </summary>
17+
public class McpFeatureWarnTestFixture : IDisposable
18+
{
19+
private readonly WebApplication _app;
20+
private readonly LogCollector _logCollector = new();
21+
22+
public IReadOnlyList<LogEntry> StartupLogs { get; }
23+
24+
public McpFeatureWarnTestFixture()
25+
{
26+
Database.Create();
27+
var connectionString = Database.CreateAdditional("mcp_feature_warn_test");
28+
29+
var builder = WebApplication.CreateBuilder();
30+
builder.WebHost.UseUrls("http://127.0.0.1:0");
31+
builder.Logging.ClearProviders();
32+
builder.Logging.SetMinimumLevel(LogLevel.Trace);
33+
builder.Logging.AddProvider(new CollectingLoggerProvider(_logCollector));
34+
_app = builder.Build();
35+
36+
_app.UseNpgsqlRest(new NpgsqlRestOptions(connectionString)
37+
{
38+
IncludeSchemas = ["mcp_warn"],
39+
CommentsMode = CommentsMode.OnlyAnnotated,
40+
EndpointCreateHandlers = [new Mcp(new McpOptions { Enabled = true })]
41+
});
42+
43+
_app.StartAsync().GetAwaiter().GetResult();
44+
StartupLogs = _logCollector.Snapshot();
45+
}
46+
47+
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
48+
public void Dispose()
49+
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
50+
{
51+
_app.StopAsync().GetAwaiter().GetResult();
52+
_app.DisposeAsync().GetAwaiter().GetResult();
53+
}
54+
}

changelog/v3.17.0.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ NpgsqlRest can now project explicitly opted-in PostgreSQL routines as **MCP tool
2323
- **MCP-only (no HTTP route):** compose with the existing `internal` annotation (`mcp` + `internal`) — the routine becomes an MCP tool with no REST endpoint. All other annotations (`authorize`, parameter handling, etc.) apply equally.
2424
- **Single Streamable-HTTP JSON-RPC endpoint** (default `/mcp`, POST). Implements the lifecycle (`initialize` advertising the `tools` capability + `serverInfo`, `notifications/initialized``202`, `ping`), `tools/list` (catalog with JSON-Schema `inputSchema` derived from the routine's parameters), and `tools/call`.
2525
- **`tools/call` executes the real routine** through the same invocation pipeline as the HTTP endpoint, **forwarding the authenticated `ClaimsPrincipal`** so `authorize` role checks apply. Results use the MCP envelope (`{ "content": [{ "type": "text", "text": … }], "isError": … }`). Two error channels per spec: business failures → `isError: true` in the result; structural failures (unknown method, unknown tool, parse error) → JSON-RPC errors (`-32601` / `-32602` / `-32700`).
26-
- **AOT-safe.** The JSON-RPC layer is hand-rolled over `System.Text.Json.Nodes` with a source-generated `JsonSerializerContext` — no reflection-based serialization, consistent with the project's `PublishAot` build.
26+
- **AOT-safe.** The JSON-RPC layer is hand-rolled over `System.Text.Json.Nodes` with a source-generated `JsonSerializerContext` — no reflection-based serialization, consistent with the project's `PublishAot` build (verified via `dotnet publish -p:PublishAot=true`).
27+
- **Diagnostics.** A routine annotated `mcp` that also uses a feature with no MCP-tool equivalent (`login`, `logout`, basic auth, `upload`, SSE) logs a build-time warning — the tool is still exposed, but the warning flags that it likely won't behave as expected over JSON-RPC.
2728
- **OAuth 2.1 Resource Server (bring-your-own Authorization Server).** Token validation reuses the host's bearer authentication; NpgsqlRest is not an Authorization Server. Configured under `McpOptions:Authorization`:
2829
- **Protected Resource Metadata (RFC 9728)** is served at `/.well-known/oauth-protected-resource{UrlPath}` (advertising `resource`, `authorization_servers`, optional `scopes_supported`, `bearer_methods_supported`) whenever an Authorization Server is configured.
2930
- **`RequireAuthorization`** gates the endpoint: an unauthenticated request gets **HTTP 401** with `WWW-Authenticate: Bearer resource_metadata="…"` (RFC 9728 §5.1) so the client can discover the AS. The PRM document itself stays anonymous. The `resource` value defaults to the request origin + `UrlPath` (overridable via `Audience`, RFC 8707).

plugins/NpgsqlRest.Mcp/Mcp.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public void Handle(RoutineEndpoint endpoint)
8989

9090
var tool = BuildTool(endpoint, info);
9191
var name = tool["name"]!.GetValue<string>();
92+
WarnIfNonApplicableFeature(endpoint, name);
9293
if (_tools.TryAdd(name, tool))
9394
{
9495
_toolEndpoints[name] = endpoint;
@@ -102,6 +103,28 @@ public void Handle(RoutineEndpoint endpoint)
102103
}
103104
}
104105

106+
/// <summary>
107+
/// Warns when a routine opted in with <c>mcp</c> also carries a feature that does not translate to an
108+
/// MCP tool call (auth flows, file upload, SSE). The tool is still exposed — this only flags that it
109+
/// likely won't behave as expected over JSON-RPC.
110+
/// </summary>
111+
private static void WarnIfNonApplicableFeature(RoutineEndpoint endpoint, string toolName)
112+
{
113+
var feature =
114+
endpoint.Login ? "login" :
115+
endpoint.Logout ? "logout" :
116+
endpoint.BasicAuth is not null ? "basic auth" :
117+
endpoint.Upload ? "upload" :
118+
endpoint.SseEventsPath is not null ? "server-sent events" :
119+
null;
120+
if (feature is not null)
121+
{
122+
Logger?.LogWarning(
123+
"MCP tool '{Name}' ({Schema}.{Routine}) is annotated `mcp` but uses a feature that does not apply to MCP tools ({Feature}). The tool is exposed but may not behave as expected over JSON-RPC.",
124+
toolName, endpoint.Routine.Schema, endpoint.Routine.Name, feature);
125+
}
126+
}
127+
105128
private JsonObject BuildTool(RoutineEndpoint endpoint, McpToolInfo info)
106129
{
107130
var routine = endpoint.Routine;

plugins/NpgsqlRest.Mcp/McpServer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ private async Task HandleToolCallAsync(HttpContext context, JsonNode? request, J
243243
// Business/execution outcome → a normal result with isError; the routine's response is the
244244
// text content verbatim. (Only structural problems above use a JSON-RPC error.)
245245
var content = new JsonArray();
246-
content.Add(new JsonObject { ["type"] = "text", ["text"] = invoke.Body ?? string.Empty });
246+
// Cast to JsonNode? to bind the non-generic JsonArray.Add (the generic Add<T> is not AOT/trim-safe).
247+
content.Add((JsonNode?)new JsonObject { ["type"] = "text", ["text"] = invoke.Body ?? string.Empty });
247248

248249
var toolResult = new JsonObject
249250
{

0 commit comments

Comments
 (0)