Defining providers
Tharga.Mcp gives consumers two ways to expose tools and resources, and they coexist on the same endpoint:
| Path | When to choose |
|---|---|
[McpServerTool] attribute on a method |
Static set of tools known at compile time. The SDK's recommended path. |
IMcpToolProvider / IMcpResourceProvider |
Tools and resources discovered at runtime — e.g. one resource per MongoDB collection, one tool per Fortnox table. |
Attribute-based tools take priority during dispatch; provider methods run only for names the attribute collection doesn't claim.
Implementing a tool provider
using System.Text.Json;
using Tharga.Mcp;
public sealed class TimeToolProvider : IMcpToolProvider
{
public McpScope Scope => McpScope.System;
public Task<IReadOnlyList<McpToolDescriptor>> ListToolsAsync(IMcpContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<McpToolDescriptor>>(
[new McpToolDescriptor
{
Name = "time_now",
Description = "Returns the current UTC time in ISO-8601 format.",
}]);
public Task<McpToolResult> CallToolAsync(string name, JsonElement args, IMcpContext context, CancellationToken ct)
{
return name switch
{
"time_now" => Task.FromResult(new McpToolResult
{
Content = [new McpContent { Text = DateTimeOffset.UtcNow.ToString("O") }],
}),
_ => Task.FromResult(new McpToolResult
{
IsError = true,
Content = [new McpContent { Text = $"Unknown tool: {name}" }],
}),
};
}
}
Register it:
builder.Services.AddThargaMcp(mcp =>
{
mcp.AddToolProvider<TimeToolProvider>();
});
Implementing a resource provider
public sealed class GreetingResourceProvider : IMcpResourceProvider
{
public McpScope Scope => McpScope.User;
public Task<IReadOnlyList<McpResourceDescriptor>> ListResourcesAsync(IMcpContext context, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<McpResourceDescriptor>>(
[new McpResourceDescriptor { Uri = "hello://greeting", Name = "Greeting" }]);
public Task<McpResourceContent> ReadResourceAsync(string uri, IMcpContext context, CancellationToken ct)
=> Task.FromResult(new McpResourceContent
{
Uri = uri,
Text = $"hello {context.UserId ?? "anonymous"}",
});
}
// Registration
mcp.AddResourceProvider<GreetingResourceProvider>();
How dispatch works
When a client calls tools/list or tools/call, the internal McpProviderDispatcher:
- Reads
IMcpContextAccessor.Currentfrom the per-request service provider. - Filters registered providers by scope hierarchy (
p.Scope <= current.Scope) — see Scopes. - For
tools/list: aggregatesListToolsAsyncresults from every surviving provider intoListToolsResult. - For
tools/call: walks providers in registration order, asks each for its current tool list, and delegatesCallToolAsyncto the first one that claims the requested name. Unknown tool →CallToolResult { IsError = true }.
Resources work the same way (ListResourcesAsync / ReadResourceAsync); unknown URI throws.
The dispatcher fills McpServerOptions.Handlers.{ListTools,CallTool,ListResources,ReadResource}Handler slots that the SDK leaves null by default — using ??=, so it never overwrites a handler a consumer has already set.
The IMcpContext parameter
Every provider method receives an IMcpContext. Phase 0 contains four fields:
public interface IMcpContext
{
string UserId { get; }
string TeamId { get; }
bool IsDeveloper { get; }
McpScope Scope { get; }
}
In a Tharga.Mcp-only host (no Platform bridge), Current is null and providers receive a fallback context (Scope = System, all other fields null). When Tharga.Platform.Mcp is in the pipeline, Current is populated per-request from the authenticated principal.
Choosing between attributes and providers
Use attributes when:
- The tool set is fixed at compile time.
- The parameters are known types — the SDK auto-generates the JSON schema from your method signature.
Use IMcpToolProvider when:
- The tool set depends on runtime data (e.g. one tool per MongoDB collection).
- You need to short-circuit
ListToolsAsyncbased on context (skip a tool if the caller can't see it). - The provider package is meant to be composable — consumers can declare what providers they want via
mcp.AddX()and not have to import your tool classes.
Both paths can coexist in the same AddThargaMcp(...) block.