SDK Enterprise Patterns
Purpose
This area describes patterns for using the SDK in production and multi-tenant environments.
Main Capabilities
- API key authentication with safe rotation
- OAuth2 client credentials for regulated environments
- Per-tenant client instances
- Audit logging via client decorator
- Human approval before high-risk tool calls
API Key Authentication
Read the key from an environment variable or a secret store. Never hardcode it.
To rotate: update the environment variable and restart the process. No code change is required. The SDK has no key lifecycle — rotation is a deployment concern.
// .NET
.WithApiKey(Environment.GetEnvironmentVariable("APIMAPPER_API_KEY")!);
// TypeScript
credentials: new ApiKeyCredentialProvider(process.env.APIMAPPER_API_KEY!)
# Python
credentials=ApiKeyCredentialProvider(os.getenv("APIMAPPER_API_KEY"))
// Java
.credentials(new ApiKeyCredentialProvider(System.getenv("APIMAPPER_API_KEY")))
OAuth2 Client Credentials
Use OAuth2 when operating in a regulated environment where all identities must originate from a trusted IdP. The SDK handles token acquisition and refresh automatically.
See SDK Authentication for full configuration examples across all languages.
Tenant-Aware Tool Filtering
In SaaS applications where one backend serves multiple tenants, create a separate client instance per tenant. Each instance maintains an independent MCP session. There is no cross-tenant data sharing.
// .NET — factory pattern
public IAiApiMapperClient GetClient(Guid tenantId, string clientId)
{
var httpClient = _httpFactory.CreateClient();
httpClient.BaseAddress = new Uri(
$"{_baseUrl.TrimEnd('/')}/runtime/{tenantId:D}/{clientId}/mcp");
return new ApiMapperClient(
httpClient,
Options.Create(new ApiMapperClientOptions
{
BaseUrl = _baseUrl,
TenantId = tenantId,
ClientId = clientId,
}),
_credentials,
_loggerFactory.CreateLogger<ApiMapperClient>());
}
// TypeScript — one client per tenant
function makeClient(tenantId: string, clientId: string) {
return new ApiMapperClient({
baseUrl: process.env.APIMAPPER_BASE_URL!,
tenantId,
clientId,
credentials: new ApiKeyCredentialProvider(process.env.APIMAPPER_API_KEY!),
});
}
# Python — one client per tenant
def make_client(tenant_id: str, client_id: str) -> ApiMapperClient:
return ApiMapperClient(ApiMapperClientOptions(
base_url=os.environ["APIMAPPER_BASE_URL"],
tenant_id=uuid.UUID(tenant_id),
client_id=client_id,
credentials=ApiKeyCredentialProvider(os.environ["APIMAPPER_API_KEY"]),
))
Audit Logging
Wrap IAiApiMapperClient with a decorator that logs tool name, elapsed time, and result status before and after every invocation. Arguments are not logged to avoid inadvertent PII or secret exposure.
// .NET decorator
public sealed class AuditingApiMapperClient : IAiApiMapperClient
{
public async Task<McpToolCallResult?> InvokeToolAsync(
string toolName, object? arguments, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
try
{
var result = await _inner.InvokeToolAsync(toolName, arguments, ct);
_logger.LogInformation(
"Tool {Tool} completed. IsError={IsError} ElapsedMs={Ms}",
toolName, result?.IsError, sw.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Tool {Tool} failed after {Ms}ms.", toolName, sw.ElapsedMilliseconds);
throw;
}
}
// delegate GetSystemPromptAsync, GetToolsAsync, ResetSession to _inner
}
// TypeScript decorator
class AuditingApiMapperClient {
async invokeTool(toolName: string, args?: Record<string, unknown>) {
const start = Date.now();
try {
const result = await this.inner.invokeTool(toolName, args);
console.log(JSON.stringify({
event: "tool_invoked", tool: toolName,
success: true, isError: result?.isError ?? null,
elapsedMs: Date.now() - start,
}));
return result;
} catch (e) {
console.error(JSON.stringify({
event: "tool_failed", tool: toolName,
elapsedMs: Date.now() - start, error: String(e),
}));
throw e;
}
}
}
Human Approval Before Tool Call
Suspend tool execution and wait for an out-of-band approval signal before invoking high-risk tools. The approval can come from any source — a SignalR message, a database poll, or an HTTP callback.
// .NET — approval interceptor
public sealed class HumanApprovalApiMapperClient : IAiApiMapperClient
{
// Tools in this set require human approval before execution.
public IReadOnlySet<string> RequiresApproval { get; init; } =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public async Task<McpToolCallResult?> InvokeToolAsync(
string toolName, object? arguments, CancellationToken ct = default)
{
if (RequiresApproval.Contains(toolName))
{
var approved = await WaitForApprovalAsync(toolName, ct);
if (!approved) throw new OperationCanceledException($"'{toolName}' was denied.");
}
return await _inner.InvokeToolAsync(toolName, arguments, ct);
}
}
The WaitForApprovalAsync implementation is application-specific. A minimal approach uses a TaskCompletionSource<bool> keyed by approval ID, resolved when the operator confirms or denies via any channel.
