Skip to content

Commit 00d9a17

Browse files
committed
Add BedrockGovernedMcpServer sample for AWS.Bedrock.MAG
Demonstrates the package's primary MCP path end-to-end: AddMcpServer().WithGovernance(...).WithBedrockGovernance(...), with the toolkit's GovernanceKernel governed by the Bedrock Guardrails policy backend, PII sanitization, and CloudWatch audit. Runs against real AWS in two modes: inline checks (InvokeGuardrailChecks, no guardrail resource) by default, or full guardrail + output PII redaction when MAG_GUARDRAIL_ID is set. Verified end-to-end over stdio (benign call allowed, SSN-in-argument denied by the Bedrock backend). README documents two behaviors found while building: WithGovernance denies all calls unless RequireAuthenticatedAgentId is relaxed or an AgentIdResolver is set, and prompt-attack/content-filter inline checks false-positive on the JSON tool-call context (use a prose ContextSerializer).
1 parent 2bf391f commit 00d9a17

7 files changed

Lines changed: 228 additions & 0 deletions

File tree

AWS.DotNetAI.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<Project Path="sampleapps/NativeAotAnnotations/NativeAotAnnotations.csproj" />
1010
<Project Path="sampleapps/AspireAppHost/AspireAppHost.csproj" />
1111
<Project Path="sampleapps/RemoteMcpAgent/RemoteMcpAgent.csproj" />
12+
<Project Path="sampleapps/BedrockGovernedMcpServer/BedrockGovernedMcpServer.csproj" />
1213
<Project Path="sampleapps/ServiceDefaults/ServiceDefaults.csproj" />
1314
</Folder>
1415
<Folder Name="/src/">
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Fallback error log written by AWS.Logger.Core (the CloudWatch audit sink) when it can't reach CloudWatch.
2+
aws-logger-errors.txt
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<!-- net8.0 to match AWS.Bedrock.MAG, which is net8.0-only (the toolkit targets net8.0). -->
6+
<TargetFramework>net8.0</TargetFramework>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\AWS.Bedrock.MAG\AWS.Bedrock.MAG.csproj" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<!-- Microsoft's toolkit MCP adapter: provides .WithGovernance() on IMcpServerBuilder. -->
17+
<PackageReference Include="Microsoft.AgentGovernance.Extensions.ModelContextProtocol" Version="5.0.0" />
18+
<!-- Generic host so the MCP stdio transport and MAG's startup hosted service run. -->
19+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<None Update="policies\mcp.yaml" CopyToOutputDirectory="PreserveNewest" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Sample: an MCP server governed by Microsoft's Agent Governance Toolkit with the AWS Bedrock backends
5+
// layered on top. It follows the primary path from the AWS.Bedrock.MAG README:
6+
//
7+
// AddMcpServer().WithGovernance(...).WithBedrockGovernance(...)
8+
//
9+
// .WithGovernance(...) comes from Microsoft.AgentGovernance.Extensions.ModelContextProtocol and
10+
// registers the toolkit's GovernanceKernel + policy engine on the MCP server.
11+
// .WithBedrockGovernance(...) comes from AWS.Bedrock.MAG and attaches the Bedrock Guardrails policy
12+
// backend, Bedrock PII sanitization, and the CloudWatch audit sink.
13+
//
14+
// See README.md for prerequisites, IAM, and how to drive it.
15+
16+
using Amazon;
17+
using AgentGovernance.Extensions.ModelContextProtocol;
18+
using BedrockGovernedMcpServer.Tools;
19+
using Microsoft.Extensions.DependencyInjection;
20+
using Microsoft.Extensions.Hosting;
21+
using Microsoft.Extensions.Logging;
22+
23+
var builder = Host.CreateApplicationBuilder(args);
24+
25+
// A stdio MCP server speaks the protocol over stdout, so all logging MUST go to stderr — otherwise log
26+
// lines corrupt the protocol stream.
27+
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
28+
29+
// Region and guardrail come from the environment so the sample stays runnable without editing code.
30+
var region = RegionEndpoint.GetBySystemName(
31+
Environment.GetEnvironmentVariable("MAG_TEST_REGION") ?? "us-west-2");
32+
var guardrailId = Environment.GetEnvironmentVariable("MAG_GUARDRAIL_ID");
33+
34+
builder.Services
35+
.AddMcpServer()
36+
.WithStdioServerTransport()
37+
// Microsoft's toolkit: loads YAML policy and stands up the GovernanceKernel this server governs with.
38+
.WithGovernance(o =>
39+
{
40+
o.PolicyPaths.Add("policies/mcp.yaml");
41+
o.ServerName = "bedrock-governed-sample";
42+
o.DefaultAgentId = "did:mcp:sample-agent";
43+
44+
// The toolkit requires an authenticated agent identity by default and denies every call before it
45+
// reaches the Bedrock backend. For a self-contained sample we allow the DefaultAgentId fallback.
46+
// In production, leave this on and set an AgentIdResolver that maps your authenticated principals.
47+
o.RequireAuthenticatedAgentId = false;
48+
})
49+
// AWS backends. Two modes, chosen by whether you supply a pre-created guardrail:
50+
// MAG_GUARDRAIL_ID set -> full path: guardrail-based policy + PII sanitization of tool output + audit.
51+
// MAG_GUARDRAIL_ID unset -> inline-checks policy (InvokeGuardrailChecks, no guardrail resource) with
52+
// PII sanitization off, since inline checks detect but do not mask text.
53+
.WithBedrockGovernance(o =>
54+
{
55+
o.Region = region;
56+
o.Audit.LogGroupName = "/agent-governance/bedrock-sample";
57+
58+
if (!string.IsNullOrWhiteSpace(guardrailId))
59+
{
60+
o.Policy.GuardrailId = guardrailId; // ApplyGuardrail on tool-call input.
61+
o.EnablePiiSanitization = true; // Reuses the policy guardrail to redact tool-output PII.
62+
}
63+
else
64+
{
65+
o.EnablePiiSanitization = false;
66+
o.Policy.InlineChecks = new AWS.Bedrock.MAG.GuardrailChecksOptions
67+
{
68+
// PII detection on the (JSON) tool-call context: a call whose arguments contain an SSN or
69+
// email is denied. This is the reliable inline-check demo.
70+
SensitiveInformationEntities = { "US_SOCIAL_SECURITY_NUMBER", "EMAIL" },
71+
ConfidenceThreshold = 0.5,
72+
73+
// NOTE: content-filter and prompt-attack categories are intentionally left off here. By
74+
// default the tool-call context is serialized to compact JSON, and the prompt-attack
75+
// classifier reads that structured JSON as an injection attempt and denies benign calls. To
76+
// use those categories, project the context to prose first via Policy.ContextSerializer, e.g.:
77+
// o.Policy.ContextSerializer = ctx => $"Tool {ctx["tool"]} called with {string.Join(", ", ctx)}";
78+
// and then add:
79+
// PromptAttackCategories = { "PROMPT_INJECTION", "JAILBREAK" },
80+
// ContentFilterCategories = { "HATE", "INSULTS", "VIOLENCE" }, SeverityThreshold = 0.5,
81+
};
82+
}
83+
})
84+
.WithTools<SupportTools>();
85+
86+
await builder.Build().RunAsync();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# BedrockGovernedMcpServer
2+
3+
A minimal MCP server governed by [Microsoft's Agent Governance Toolkit](https://devblogs.microsoft.com/dotnet/announcing-agent-governance-toolkit-mcp-extensions-for-dotnet/) with the AWS backends from [`AWS.Bedrock.MAG`](../../src/AWS.Bedrock.MAG) layered on top. It shows the package's primary path:
4+
5+
```csharp
6+
builder.Services.AddMcpServer()
7+
.WithStdioServerTransport()
8+
.WithGovernance(...) // Microsoft.AgentGovernance.Extensions.ModelContextProtocol
9+
.WithBedrockGovernance(...) // AWS.Bedrock.MAG
10+
.WithTools<SupportTools>();
11+
```
12+
13+
- `.WithGovernance(...)` (Microsoft's MCP extensions package) loads the YAML policy and stands up the toolkit's `GovernanceKernel`.
14+
- `.WithBedrockGovernance(...)` (this repo) attaches the Bedrock Guardrails **policy backend**, Bedrock **PII sanitization** of tool output, and the **CloudWatch audit** sink to that kernel.
15+
16+
The server exposes two toy "customer support" tools (`Tools/SupportTools.cs`). `lookup_customer` deliberately returns PII so you can watch sanitization redact it.
17+
18+
## Two modes
19+
20+
The sample picks a mode from the environment so it runs with or without a pre-created guardrail:
21+
22+
| `MAG_GUARDRAIL_ID` | Policy | PII sanitization | AWS resource needed |
23+
|---|---|---|---|
24+
| **unset** (default) | Inline checks (`InvokeGuardrailChecks`) — PII detection on the tool-call arguments | off (inline checks detect but don't mask) | none — just IAM |
25+
| **set** to a guardrail id | `ApplyGuardrail` on the tool-call input | on — redacts PII in tool output using the same guardrail | a Bedrock guardrail |
26+
27+
## Prerequisites
28+
29+
- .NET 8 SDK.
30+
- AWS credentials in the default chain (env vars, profile, or role). Region defaults to `us-west-2` (override with `MAG_TEST_REGION`).
31+
- IAM for the mode you run:
32+
- Inline-checks mode: `bedrock:InvokeGuardrailChecks`.
33+
- Guardrail mode: `bedrock:ApplyGuardrail` on the guardrail, plus `logs:CreateLogGroup`, `logs:CreateLogStream`, `logs:PutLogEvents` (and `cloudwatch:PutMetricData` if metrics are on) for audit.
34+
35+
## Run it
36+
37+
```bash
38+
cd sampleapps/BedrockGovernedMcpServer
39+
dotnet run
40+
```
41+
42+
It's a stdio MCP server, so it talks JSON-RPC over stdin/stdout and logs to stderr. Point any MCP client at `dotnet run` in this directory, or drive it by hand — send an `initialize`, then a `tools/call`:
43+
44+
```jsonc
45+
// benign call -> allowed, tool runs
46+
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lookup_customer","arguments":{"customerId":"C-1024"}}}
47+
48+
// argument contains an SSN -> denied by the Bedrock policy backend
49+
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lookup_customer","arguments":{"customerId":"my ssn is 123-45-6789"}}}
50+
```
51+
52+
In default (inline-checks) mode the first returns the customer record and the second comes back with `isError: true` and `"External policy backend 'bedrock-guardrails' denied the request."`
53+
54+
To exercise the full path (guardrail policy + output PII redaction + CloudWatch audit), create a Bedrock guardrail that anonymizes `US_SOCIAL_SECURITY_NUMBER` and run with `MAG_GUARDRAIL_ID=<id> dotnet run`. The `lookup_customer` output SSN then comes back redacted.
55+
56+
## Two things worth knowing (learned building this sample)
57+
58+
1. **`WithGovernance` requires an authenticated agent identity by default.** With it on, every call is denied before reaching Bedrock. This sample sets `RequireAuthenticatedAgentId = false` to use the `DefaultAgentId` fallback. In production, leave it on and set an `AgentIdResolver` that maps your authenticated principals.
59+
2. **Prompt-attack / content-filter inline checks false-positive on the JSON tool-call context.** The tool-call context is serialized to compact JSON by default, and the prompt-attack classifier reads that structure as an injection attempt — denying benign calls. This sample uses PII inline checks (which score cleanly on JSON). To use prompt-attack or content-filter categories, project the context to prose first via `Policy.ContextSerializer` (see the commented example in `Program.cs`).
60+
61+
## Package versions
62+
63+
`AWS.Bedrock.MAG` pins `ModelContextProtocol` 2.1.0; `Microsoft.AgentGovernance.Extensions.ModelContextProtocol` 5.0.0 builds against 1.4.1. NuGet unifies to 2.1.0 and the two compose on the same `IMcpServerBuilder` — verified by this sample building and running end-to-end.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.ComponentModel;
5+
using ModelContextProtocol.Server;
6+
7+
namespace BedrockGovernedMcpServer.Tools;
8+
9+
/// <summary>
10+
/// A tiny "customer support" MCP tool surface used to exercise the two Bedrock governance paths:
11+
/// <list type="bullet">
12+
/// <item>Policy evaluation runs on the tool-call <b>input</b> (tool name + arguments), so passing a
13+
/// blocked word or PII in an argument lets the Bedrock policy backend deny the call.</item>
14+
/// <item>PII sanitization runs on the tool-call <b>output</b> text, so <see cref="LookupCustomer"/>
15+
/// deliberately returns an SSN to show it redacted (when a guardrail is configured).</item>
16+
/// </list>
17+
/// </summary>
18+
[McpServerToolType]
19+
public sealed class SupportTools
20+
{
21+
[McpServerTool(Name = "lookup_customer")]
22+
[Description("Look up a customer's contact record by their customer id.")]
23+
public string LookupCustomer(
24+
[Description("The customer id, e.g. \"C-1024\".")] string customerId)
25+
{
26+
// Returns PII in the text block on purpose: with a guardrail configured the Bedrock sanitizer
27+
// redacts the SSN before this reaches the caller. See the README for what to expect.
28+
return $"Customer {customerId}: Jane Doe, jane.doe@example.com, SSN 123-45-6789, status ACTIVE.";
29+
}
30+
31+
[McpServerTool(Name = "get_account_balance")]
32+
[Description("Get the current account balance for a customer.")]
33+
public string GetAccountBalance(
34+
[Description("The customer id, e.g. \"C-1024\".")] string customerId)
35+
{
36+
return $"Customer {customerId} balance: $482.10 USD.";
37+
}
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Microsoft Agent Governance Toolkit policy for the MCP server.
2+
#
3+
# This baseline lets every tool call through at the toolkit layer (default_action: allow) and leaves the
4+
# real decisions to the AWS Bedrock policy backend added by .WithBedrockGovernance(...): guardrail /
5+
# inline-check evaluation of each tool call. That keeps the sample focused on the Bedrock integration.
6+
#
7+
# To also gate tools at the toolkit layer, add entries under `rules:` (see the toolkit docs for the rule
8+
# schema: name, action, priority, condition).
9+
apiVersion: governance.toolkit/v1
10+
name: bedrock-governed-sample
11+
default_action: allow
12+
rules: []

0 commit comments

Comments
 (0)