Skip to content

onboardiq/mcp-authorization-dotnet

Repository files navigation

McpAuthorization

In-process MCP server for .NET with permission-based schema shaping. Authorization is schema discrimination — different users get different JSON Schemas from the same tool definitions.

C# port of the mcp_authorization Ruby gem and TypeScript package.

Install

dotnet add package McpAuthorization

How it works

Traditional MCP authorization shows every tool to every user and rejects unauthorized calls at runtime. McpAuthorization takes a different approach: the LLM never sees what the user can't do. Fields, tools, and even descriptions are pruned from the schema before the model receives it.

Three layers of authorization, each independently controlled:

Layer What it controls How you declare it
Tool visibility Entire tool hidden from tools/list Authorization = "permission" on ToolDefinition
Field visibility Property pruned from input schema [Requires("permission")] on record property
Dynamic defaults Default value injected per-user [DefaultFor("key")] on record property

Quick start

1. Define shapes with permission-gated fields

using McpAuthorization;

public record PatientQuery
{
    [McpProperty("Search term")]
    public required string Search { get; init; }

    [McpProperty("Include SSN in results")]
    [Requires("SecurityAdmin")]          // only admins see this field
    public bool IncludeSsn { get; init; }
}

2. Implement IAuthContext for your auth system

public class MyAuthContext(HashSet<string> permissions) : IAuthContext
{
    public bool Can(string permission) => permissions.Contains(permission);
}

3. Register tools and run

var server = new McpAuthorizationServer("my-app", "1.0.0");

server.Register(new ToolDefinition<PatientQuery>
{
    Name = "search_patients",
    Description = "Search patient records",
    Authorization = "ViewPatients",      // tool-level gate
    ReadOnly = true,
    Handler = async (input, ctx, ct) =>
    {
        // input.IncludeSsn is only present if user has SecurityAdmin
        return new { results = new[] { "..." } };
    },
});

await server.RunAsync(new MyAuthContext(["ViewPatients"]));

Claude Desktop config:

{
  "mcpServers": {
    "my-app": {
      "command": "dotnet",
      "args": ["run", "--project", "path/to/MyApp"]
    }
  }
}

Attributes

Attribute Target Effect
[Requires("perm")] Property Field pruned from schema if user lacks permission
[DependsOn("field")] Property Emits dependentRequired in JSON Schema
[DefaultFor("key")] Property Populates default from IAuthContext.DefaultFor()
[McpProperty("desc")] Property Sets description, format, min/maxLength, pattern
[McpDeprecated] Property Marks field as deprecated in schema

Dynamic descriptions

Tool descriptions can change based on user permissions, matching the Ruby gem's def description pattern:

server.Register(new ToolDefinition<AdvanceInput>
{
    Name = "advance_step",
    Description = "Advance to the next stage",
    DynamicDescription = ctx => ctx.Can("BackwardRouting")
        ? "Advance an applicant to any stage, or reroute them backward."
        : "Advance an applicant to the next stage.",
    Authorization = "ManageWorkflows",
    Handler = async (input, ctx, ct) => { ... },
});

SqlSafetyGuard

Reusable utility for validating raw SQL before execution. Uses a state-machine tokenizer (not regex) to handle comment-hiding attacks, string literal embedding, and MySQL-specific quoting.

var result = SqlSafetyGuard.Validate(userProvidedSql);
if (!result.IsValid)
{
    // result.Violations contains specific reasons
    return new { error = true, violations = result.Violations };
}
// result.Sql is cleaned (comments stripped)
var table = db.ExecuteQuery(result.Sql + " LIMIT 100");

Blocks: INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, semicolons, INTO OUTFILE, SLEEP(), BENCHMARK(), @@ system variables, and more. See tests for the full list.

Schema compilation

The SchemaCompiler reads record properties and attributes via reflection once at registration time, then caches the metadata. Per-request work is only permission checks (HashSet.Contains calls) and JsonObject construction.

Phase 1 (cached):  Type → reflection → List<PropertyMeta>
Phase 2 (per-req): PropertyMeta + IAuthContext.Can() → pruned JsonObject

Supported type mappings:

C# type JSON Schema
string {"type": "string"}
int, long {"type": "integer"}
decimal, double {"type": "number"}
bool {"type": "boolean"}
DateTime {"type": "string", "format": "date-time"}
DateOnly {"type": "string", "format": "date"}
enum {"type": "string", "enum": [...]}
T[], List<T> {"type": "array", "items": {...}}
nested record Recurses (with permission pruning)

Example: OpenDental integration

The examples/OpenDentalExample/ directory shows a complete integration:

  • Shapes.cs — Domain records with [Requires] attributes
  • OpenDentalBridge.csIAuthContext querying permission tables, IDataAccess wrapping DataCore.GetTable()
  • Program.cs — Tool registration and StdIO server

Run the demo to see schema differences per role:

cd examples/OpenDentalExample
dotnet run -- --demo

Output shows Front Desk sees 2 tools, Office Manager sees 3 (with extra fields), Admin sees all 4 (including raw SQL and SSN access).

Cross-language pattern

The same authorization-as-schema-shaping pattern, three implementations:

Concept Ruby gem TypeScript package C# (this package)
Auth contract can?(:symbol) ctx.can('string') IAuthContext.Can(string)
Field gating @requires(:flag) .requires('perm') [Requires("perm")]
Dependency @depends_on(:field) .dependsOn('field') [DependsOn("field")]
Dynamic default @default_for(:key) .defaultFor('key') [DefaultFor("key")]
Tool gate authorization :flag authorization: 'perm' Authorization = "perm"
Materialization ToolRegistry.tool_classes_for registry.materialize(ctx) registry.Materialize(ctx)

License

MIT

About

In-process MCP server with permission-based schema shaping for .NET. Authorization is schema discrimination — different users get different JSON Schemas from the same tool definitions.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages