Skip to content

Commit 6ca36e9

Browse files
committed
Add ComplianceServer sample
1 parent 1bef31f commit 6ca36e9

10 files changed

Lines changed: 606 additions & 0 deletions

File tree

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
4343
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
4444
<Project Path="samples/ChatWithTools/ChatWithTools.csproj" />
45+
<Project Path="samples/ComplianceServer/ComplianceServer.csproj" />
4546
<Project Path="samples/EverythingServer/EverythingServer.csproj" />
4647
<Project Path="samples/InMemoryTransport/InMemoryTransport.csproj" />
4748
<Project Path="samples/ProtectedMcpClient/ProtectedMcpClient.csproj" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@HostAddress = http://localhost:3001
2+
3+
POST {{HostAddress}}/
4+
Accept: application/json, text/event-stream
5+
Content-Type: application/json
6+
7+
{
8+
"jsonrpc": "2.0",
9+
"id": 1,
10+
"method": "ping"
11+
}
12+
13+
###
14+
15+
POST {{HostAddress}}/
16+
Accept: application/json, text/event-stream
17+
Content-Type: application/json
18+
19+
{
20+
"jsonrpc": "2.0",
21+
"id": 2,
22+
"method": "initialize",
23+
"params": {
24+
"clientInfo": {
25+
"name": "RestClient",
26+
"version": "0.1.0"
27+
},
28+
"capabilities": {},
29+
"protocolVersion": "2025-06-18"
30+
}
31+
}
32+
33+
###
34+
35+
@SessionId = XxIXkrK210aKVnZxD8Iu_g
36+
37+
POST {{HostAddress}}/
38+
Accept: application/json, text/event-stream
39+
Content-Type: application/json
40+
MCP-Protocol-Version: 2025-06-18
41+
Mcp-Session-Id: {{SessionId}}
42+
43+
{
44+
"jsonrpc": "2.0",
45+
"id": 3,
46+
"method": "tools/list"
47+
}
48+
49+
###
50+
51+
POST {{HostAddress}}/
52+
Accept: application/json, text/event-stream
53+
Content-Type: application/json
54+
MCP-Protocol-Version: 2025-06-18
55+
Mcp-Session-Id: {{SessionId}}
56+
57+
{
58+
"jsonrpc": "2.0",
59+
"id": 4,
60+
"method": "resources/list"
61+
}
62+
63+
###
64+
65+
POST {{HostAddress}}/
66+
Accept: application/json, text/event-stream
67+
Content-Type: application/json
68+
MCP-Protocol-Version: 2025-06-18
69+
Mcp-Session-Id: {{SessionId}}
70+
71+
{
72+
"jsonrpc": "2.0",
73+
"id": 5,
74+
"method": "prompts/list"
75+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using ComplianceServer;
2+
using ComplianceServer.Prompts;
3+
using ComplianceServer.Resources;
4+
using ComplianceServer.Tools;
5+
using Microsoft.Extensions.AI;
6+
using ModelContextProtocol;
7+
using ModelContextProtocol.Protocol;
8+
using ModelContextProtocol.Server;
9+
using System.Collections.Concurrent;
10+
11+
var builder = WebApplication.CreateBuilder(args);
12+
13+
// Dictionary of session IDs to a set of resource URIs they are subscribed to
14+
// The value is a ConcurrentDictionary used as a thread-safe HashSet
15+
// because .NET does not have a built-in concurrent HashSet
16+
ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> subscriptions = new();
17+
18+
builder.Services
19+
.AddMcpServer()
20+
.WithHttpTransport()
21+
.WithTools<ComplianceTools>()
22+
.WithPrompts<CompliancePrompts>()
23+
.WithResources<ComplianceResources>()
24+
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
25+
{
26+
if (ctx.Server.SessionId == null)
27+
{
28+
throw new McpException("Cannot add subscription for server with null SessionId");
29+
}
30+
if (ctx.Params?.Uri is { } uri)
31+
{
32+
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
33+
34+
await ctx.Server.SampleAsync([
35+
new ChatMessage(ChatRole.System, "You are a helpful test server"),
36+
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
37+
],
38+
options: new ChatOptions
39+
{
40+
MaxOutputTokens = 100,
41+
Temperature = 0.7f,
42+
},
43+
cancellationToken: ct);
44+
}
45+
46+
return new EmptyResult();
47+
})
48+
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
49+
{
50+
if (ctx.Server.SessionId == null)
51+
{
52+
throw new McpException("Cannot remove subscription for server with null SessionId");
53+
}
54+
if (ctx.Params?.Uri is { } uri)
55+
{
56+
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
57+
}
58+
return new EmptyResult();
59+
})
60+
.WithCompleteHandler(async (ctx, ct) =>
61+
{
62+
var exampleCompletions = new Dictionary<string, IEnumerable<string>>
63+
{
64+
{ "style", ["casual", "formal", "technical", "friendly"] },
65+
{ "temperature", ["0", "0.5", "0.7", "1.0"] },
66+
{ "resourceId", ["1", "2", "3", "4", "5"] }
67+
};
68+
69+
if (ctx.Params is not { } @params)
70+
{
71+
throw new NotSupportedException($"Params are required.");
72+
}
73+
74+
var @ref = @params.Ref;
75+
var argument = @params.Argument;
76+
77+
if (@ref is ResourceTemplateReference rtr)
78+
{
79+
var resourceId = rtr.Uri?.Split("/").Last();
80+
81+
if (resourceId is null)
82+
{
83+
return new CompleteResult();
84+
}
85+
86+
var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value));
87+
88+
return new CompleteResult
89+
{
90+
Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() }
91+
};
92+
}
93+
94+
if (@ref is PromptReference pr)
95+
{
96+
if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable<string>? value))
97+
{
98+
throw new NotSupportedException($"Unknown argument name: {argument.Name}");
99+
}
100+
101+
var values = value.Where(value => value.StartsWith(argument.Value));
102+
return new CompleteResult
103+
{
104+
Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() }
105+
};
106+
}
107+
108+
throw new NotSupportedException($"Unknown reference type: {@ref.Type}");
109+
})
110+
.WithSetLoggingLevelHandler(async (ctx, ct) =>
111+
{
112+
if (ctx.Params?.Level is null)
113+
{
114+
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
115+
}
116+
117+
// The SDK updates the LoggingLevel field of the IMcpServer
118+
119+
await ctx.Server.SendNotificationAsync("notifications/message", new
120+
{
121+
Level = "debug",
122+
Logger = "test-server",
123+
Data = $"Logging level set to {ctx.Params.Level}",
124+
}, cancellationToken: ct);
125+
126+
return new EmptyResult();
127+
});
128+
129+
var app = builder.Build();
130+
131+
app.MapMcp();
132+
133+
app.Run();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using ModelContextProtocol.Server;
2+
using ModelContextProtocol.Protocol;
3+
using Microsoft.Extensions.AI;
4+
using System.ComponentModel;
5+
6+
namespace ComplianceServer.Prompts;
7+
8+
public class CompliancePrompts
9+
{
10+
// Sample base64 encoded 1x1 red PNG pixel for testing
11+
const string TEST_IMAGE_BASE64 =
12+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
13+
14+
[McpServerPrompt(Name = "test_simple_prompt"), Description("Simple prompt without arguments")]
15+
public static string SimplePrompt() => "This is a simple prompt without arguments";
16+
17+
[McpServerPrompt(Name = "test_prompt_with_arguments"), Description("Parameterized prompt")]
18+
public static IEnumerable<ChatMessage> ParameterizedPrompt(
19+
[Description("First test argument")] string arg1,
20+
[Description("Second test argument")] string arg2)
21+
{
22+
return [
23+
new ChatMessage(ChatRole.User,$"Prompt with arguments: arg1={arg1}, arg2={arg2}"),
24+
];
25+
}
26+
27+
[McpServerPrompt(Name = "test_prompt_with_embedded_resource"), Description("Prompt with embedded resource")]
28+
public static IEnumerable<PromptMessage> PromptWithEmbeddedResource(
29+
[Description("URI of the resource to embed")] string resourceUri)
30+
{
31+
return [
32+
new PromptMessage
33+
{
34+
Role = Role.User,
35+
Content = new EmbeddedResourceBlock
36+
{
37+
Resource = new TextResourceContents
38+
{
39+
Uri = resourceUri,
40+
Text = "Embedded resource content for testing.",
41+
MimeType = "text/plain"
42+
}
43+
}
44+
},
45+
new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "Please process the embedded resource above." } },
46+
];
47+
}
48+
49+
[McpServerPrompt(Name = "test_prompt_with_image"), Description("Prompt with image")]
50+
public static IEnumerable<ChatMessage> PromptWithImage()
51+
{
52+
return [
53+
new ChatMessage(ChatRole.User, [new DataContent(TEST_IMAGE_BASE64)]),
54+
new ChatMessage(ChatRole.User, "Please analyze the image above."),
55+
];
56+
}
57+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": false,
8+
"applicationUrl": "http://localhost:3001",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": false,
17+
"applicationUrl": "https://localhost:7292;http://localhost:3001",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using ModelContextProtocol.Protocol;
2+
using ModelContextProtocol.Server;
3+
using System.ComponentModel;
4+
using System.Text.Json;
5+
6+
namespace ComplianceServer.Resources;
7+
8+
[McpServerResourceType]
9+
public class ComplianceResources
10+
{
11+
// Sample base64 encoded 1x1 red PNG pixel for testing
12+
private const string TEST_IMAGE_BASE64 =
13+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
14+
15+
/// <summary>
16+
/// Static text resource for testing
17+
/// </summary>
18+
[McpServerResource(UriTemplate = "test://static-text", Name = "Static Text Resource", MimeType = "text/plain")]
19+
[Description("A static text resource for testing")]
20+
public static string StaticText()
21+
{
22+
return "This is the content of the static text resource.";
23+
}
24+
25+
/// <summary>
26+
/// Static binary resource (image) for testing
27+
/// </summary>
28+
[McpServerResource(UriTemplate = "test://static-binary", Name = "Static Binary Resource", MimeType = "image/png")]
29+
[Description("A static binary resource (image) for testing")]
30+
public static BlobResourceContents StaticBinary()
31+
{
32+
return new BlobResourceContents
33+
{
34+
Uri = "test://static-binary",
35+
MimeType = "image/png",
36+
Blob = TEST_IMAGE_BASE64
37+
};
38+
}
39+
40+
/// <summary>
41+
/// Resource template with parameter substitution
42+
/// </summary>
43+
[McpServerResource(UriTemplate = "test://template/{id}/data", Name = "Resource Template", MimeType = "application/json")]
44+
[Description("A resource template with parameter substitution")]
45+
public static TextResourceContents TemplateResource(string id)
46+
{
47+
var data = new
48+
{
49+
id = id,
50+
templateTest = true,
51+
data = $"Data for ID: {id}"
52+
};
53+
54+
return new TextResourceContents
55+
{
56+
Uri = $"test://template/{id}/data",
57+
MimeType = "application/json",
58+
Text = JsonSerializer.Serialize(data)
59+
};
60+
}
61+
62+
/// <summary>
63+
/// Subscribable resource that can send updates
64+
/// </summary>
65+
[McpServerResource(UriTemplate = "test://watched-resource", Name = "Watched Resource", MimeType = "text/plain")]
66+
[Description("A resource that auto-updates every 3 seconds")]
67+
public static string WatchedResource()
68+
{
69+
return "Watched resource content";
70+
}
71+
}

0 commit comments

Comments
 (0)