MCP Server Development Standards (Optum)
Standards, patterns, and guardrails for building Model Context Protocol (MCP) servers compatible with Wall-E, VS Code Copilot, and enterprise systems.
MCP Server Development Standards
Overview
Model Context Protocol (MCP) servers provide standardized interfaces for LLM agents to interact with enterprise systems. These standards ensure security, maintainability, and compatibility with Wall-E and VS Code Copilot.
Architecture Requirements
Server Structure
MUST organize MCP servers with this structure:
mcp-server-{name}/
├── src/
│ ├── index.ts # Entry point with server setup
│ ├── tools/ # Tool implementations
│ │ ├── index.ts # Tool registry
│ │ └── {tool-name}.ts # Individual tool files
│ ├── resources/ # Resource implementations
│ │ ├── index.ts # Resource registry
│ │ └── {resource}.ts # Individual resource files
│ ├── prompts/ # Prompt templates
│ ├── types/ # TypeScript type definitions
│ └── lib/ # Shared utilities
├── schemas/ # JSON Schema definitions
├── tests/ # Test files
├── package.json
├── tsconfig.json
└── README.md
Naming Conventions
MUST follow these naming conventions:
| Element | Convention | Example |
|---|---|---|
| Server name | mcp-server-{domain} | mcp-server-servicenow |
| Tool names | kebab-case | get-incident-details |
| Resource URIs | {domain}://{type}/{id} | servicenow://incident/INC0012345 |
| Prompt names | kebab-case | triage-incident |
NEVER rename tool or resource identifiers after publication—they are API surface.
Tool Development
Tool Classification
MUST classify every tool by risk level:
type ToolRiskLevel = 'read-only' | 'low-risk-write' | 'high-risk-write';
interface ToolMetadata {
name: string;
description: string;
riskLevel: ToolRiskLevel;
requiresApproval: boolean;
auditRequired: boolean;
}
| Risk Level | Description | Examples |
|---|---|---|
| read-only | No state changes | get-incident, list-nodes, query-logs |
| low-risk-write | Reversible changes | add-comment, update-tag, create-draft |
| high-risk-write | Irreversible or sensitive | close-incident, delete-resource, execute-command |
Tool Implementation Pattern
MUST implement tools with this pattern:
import { z } from 'zod';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
// 1. Define input schema with Zod
const GetIncidentInputSchema = z.object({
incidentId: z
.string()
.regex(/^INC\d{7,10}$/, 'Must be valid incident ID format')
.describe('ServiceNow incident ID (e.g., INC0012345)'),
includeComments: z.boolean().default(false).describe('Include incident comments in response'),
});
type GetIncidentInput = z.infer<typeof GetIncidentInputSchema>;
// 2. Implement tool handler
export async function getIncident(
input: GetIncidentInput,
context: ToolContext,
): Promise<ToolResult> {
// Validate input (Zod already validated schema)
const { incidentId, includeComments } = input;
// Log tool invocation for audit
context.logger.info('tool_invocation', {
tool: 'get-incident-details',
incidentId,
correlationId: context.correlationId,
userId: context.userId,
});
try {
// Call external service
const incident = await context.servicenow.getIncident(incidentId, {
includeComments,
});
if (!incident) {
throw new McpError(ErrorCode.InvalidParams, `Incident ${incidentId} not found`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(incident, null, 2),
},
],
isError: false,
};
} catch (error) {
context.logger.error('tool_error', {
tool: 'get-incident-details',
error: error.message,
correlationId: context.correlationId,
});
throw error;
}
}
// 3. Register tool with metadata
export const getIncidentTool: ToolDefinition = {
name: 'get-incident-details',
description: 'Retrieve details of a ServiceNow incident by ID',
inputSchema: zodToJsonSchema(GetIncidentInputSchema),
metadata: {
riskLevel: 'read-only',
requiresApproval: false,
auditRequired: true,
},
handler: getIncident,
};
Write Operation Safeguards
MUST implement safeguards for write operations:
// For low-risk-write tools
export async function addIncidentComment(
input: AddCommentInput,
context: ToolContext,
): Promise<ToolResult> {
// 1. Validate user has permission
if (!context.permissions.canComment) {
throw new McpError(ErrorCode.InvalidRequest, 'User lacks permission to add comments');
}
// 2. Log before action
context.logger.info('write_operation_start', {
tool: 'add-incident-comment',
incidentId: input.incidentId,
correlationId: context.correlationId,
});
// 3. Perform operation
const result = await context.servicenow.addComment(input.incidentId, input.comment);
// 4. Log after action
context.logger.info('write_operation_complete', {
tool: 'add-incident-comment',
incidentId: input.incidentId,
commentId: result.commentId,
correlationId: context.correlationId,
});
return { content: [{ type: 'text', text: 'Comment added successfully' }] };
}
// For high-risk-write tools
export async function closeIncident(
input: CloseIncidentInput,
context: ToolContext,
): Promise<ToolResult> {
// 1. REQUIRE explicit approval token for high-risk operations
if (!input.approvalToken) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
status: 'approval_required',
message: 'Closing an incident requires explicit approval',
approvalPrompt: `Confirm closing ${input.incidentId} with resolution: ${input.resolution}`,
action: 'close-incident',
params: { incidentId: input.incidentId },
}),
},
],
isError: false,
};
}
// 2. Validate approval token
const isValid = await context.approvalService.validate(input.approvalToken);
if (!isValid) {
throw new McpError(ErrorCode.InvalidRequest, 'Invalid approval token');
}
// 3. Proceed with operation
// ...
}
Resource Development
Resource URI Format
MUST use consistent URI format:
// URI format: {protocol}://{type}/{identifier}[/{subresource}]
const resourceUri = 'servicenow://incident/INC0012345';
const subResourceUri = 'servicenow://incident/INC0012345/comments';
// Parse URI helper
function parseResourceUri(uri: string): ResourceRef {
const url = new URL(uri);
return {
protocol: url.protocol.replace(':', ''),
type: url.hostname,
id: url.pathname.split('/')[1],
subresource: url.pathname.split('/')[2] || null,
};
}
Resource Implementation
export const incidentResource: ResourceDefinition = {
uriTemplate: 'servicenow://incident/{incidentId}',
name: 'ServiceNow Incident',
description: 'A ServiceNow incident record',
mimeType: 'application/json',
async read(uri: string, context: ResourceContext): Promise<ResourceContent> {
const { id } = parseResourceUri(uri);
const incident = await context.servicenow.getIncident(id);
if (!incident) {
throw new McpError(ErrorCode.InvalidParams, `Incident ${id} not found`);
}
return {
uri,
mimeType: 'application/json',
text: JSON.stringify(incident, null, 2),
};
},
async list(context: ResourceContext): Promise<ResourceRef[]> {
const incidents = await context.servicenow.listRecentIncidents(50);
return incidents.map((inc) => ({
uri: `servicenow://incident/${inc.number}`,
name: `${inc.number}: ${inc.short_description}`,
}));
},
};
Security Requirements
Authentication and Authorization
MUST implement enterprise authentication:
// server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server(
{ name: 'mcp-server-servicenow', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } },
);
// Middleware: Validate auth on every request
server.setRequestHandler(async (request, context) => {
// 1. Extract credentials from transport
const authHeader = context.transport.headers?.authorization;
if (!authHeader) {
throw new McpError(ErrorCode.InvalidRequest, 'Missing authorization');
}
// 2. Validate token with enterprise auth service
const user = await authService.validateToken(authHeader);
if (!user) {
throw new McpError(ErrorCode.InvalidRequest, 'Invalid token');
}
// 3. Check RBAC permissions for requested operation
const hasPermission = await rbacService.checkPermission(user, request.method, request.params);
if (!hasPermission) {
throw new McpError(ErrorCode.InvalidRequest, 'Insufficient permissions');
}
// 4. Add user context for downstream use
context.userId = user.id;
context.permissions = user.permissions;
return next(request, context);
});
Secrets Management
NEVER embed secrets in code or prompts:
// ❌ NEVER do this
const apiKey = 'sk-1234567890abcdef'; // pragma: allowlist secret
// ✅ ALWAYS use environment variables
const apiKey = process.env.SERVICENOW_API_KEY;
if (!apiKey) {
throw new Error('SERVICENOW_API_KEY environment variable required');
}
// ✅ PREFER secrets manager integration
import { SecretClient } from '@azure/keyvault-secrets';
async function getApiKey(): Promise<string> {
const client = new SecretClient(process.env.KEYVAULT_URL!, new DefaultAzureCredential());
const secret = await client.getSecret('servicenow-api-key');
return secret.value!;
}
Input Sanitization
MUST sanitize all user inputs:
import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';
// Sanitize string inputs
const sanitizedString = z.string().transform((val) => {
// Remove HTML/script injection
return DOMPurify.sanitize(val, { ALLOWED_TAGS: [] });
});
// Validate and constrain queries
const QueryInputSchema = z.object({
table: z.enum(['incident', 'change_request', 'problem']),
query: z
.string()
.max(1000)
.refine((q) => !q.includes(';'), 'Query cannot contain semicolons'),
limit: z.number().int().min(1).max(100).default(25),
});
Observability Requirements
Structured Logging
MUST emit structured logs:
interface LogContext {
correlationId: string;
userId: string;
tool?: string;
resource?: string;
duration?: number;
error?: string;
}
const logger = {
info(event: string, context: LogContext) {
console.log(
JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
event,
...context,
}),
);
},
error(event: string, context: LogContext) {
console.error(
JSON.stringify({
timestamp: new Date().toISOString(),
level: 'error',
event,
...context,
}),
);
},
};
Metrics
MUST expose metrics for monitoring:
import { Counter, Histogram } from 'prom-client';
const toolInvocations = new Counter({
name: 'mcp_tool_invocations_total',
help: 'Total tool invocations',
labelNames: ['tool', 'status', 'risk_level'],
});
const toolDuration = new Histogram({
name: 'mcp_tool_duration_seconds',
help: 'Tool execution duration',
labelNames: ['tool'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
// Wrap tool handlers
async function instrumentedHandler(
tool: ToolDefinition,
input: unknown,
context: ToolContext,
): Promise<ToolResult> {
const timer = toolDuration.startTimer({ tool: tool.name });
try {
const result = await tool.handler(input, context);
toolInvocations.inc({
tool: tool.name,
status: 'success',
risk_level: tool.metadata.riskLevel,
});
return result;
} catch (error) {
toolInvocations.inc({
tool: tool.name,
status: 'error',
risk_level: tool.metadata.riskLevel,
});
throw error;
} finally {
timer();
}
}
Documentation Requirements
MUST include in README:
- Quickstart: How to install and run
- Tool Reference: Each tool with input/output schemas
- Resource Reference: Each resource with URI format
- Security Model: Auth requirements, permissions
- Error Codes: All possible errors with meanings
Example tool documentation:
## get-incident-details
Retrieve details of a ServiceNow incident.
**Risk Level**: read-only
### Input Schema
| Parameter | Type | Required | Description |
| --------------- | ------- | -------- | ----------------------------------- |
| incidentId | string | Yes | ServiceNow incident ID (INC0012345) |
| includeComments | boolean | No | Include comments (default: false) |
### Output
```json
{
"number": "INC0012345",
"short_description": "Application not responding",
"state": "In Progress",
"priority": "2 - High",
"assigned_to": "[email protected]"
}
```
Errors
| Code | Description |
|---|---|
| InvalidParams | Incident ID format invalid or not found |
| Unauthorized | User lacks permission to view incident |
## Terraform Integration
**NEVER** execute `terraform apply` from MCP tools:
```typescript
// ✅ Read-only Terraform operations allowed
export const terraformPlanTool: ToolDefinition = {
name: 'terraform-plan',
riskLevel: 'read-only',
async handler(input, context) {
// Generate plan output for review
const plan = await terraform.plan(input.workingDir);
return { content: [{ type: 'text', text: plan }] };
},
};
// ❌ NEVER implement apply in MCP
// Applies MUST go through TFE + CI/CD
Wall-E Integration
When integrating with Wall-E:
- MUST register tool risk levels with Wall-E policy engine
- MUST pass correlation IDs for distributed tracing
- MUST support kill-switch for emergency tool disabling
- SHOULD map tools to Wall-E agent capabilities
# wall-e-config.yaml
agents:
- name: servicenow-agent
mcp_server: mcp-server-servicenow
tools:
- name: get-incident-details
wall_e_policy: allow
- name: add-incident-comment
wall_e_policy: allow_with_audit
- name: close-incident
wall_e_policy: require_approval
Related Assets
Wall-E Workflow Designer (Optum)
Assist with designing, reviewing, and optimizing multi-agent Wall-E workflows and MCP integrations following Optum enterprise patterns.
Owner: epic-platform-sre
Wall-E Agent Composition Helper
Compose multiple specialized agents into a safe Wall-E workflow with proper MCP tool assignments, guardrails, and human-in-loop gates.
Owner: epic-platform-sre
Wall-E RAG Tuning Helper
Recommend RAG chunking, embedding, and retrieval parameters for Wall-E contexts based on corpus characteristics and performance requirements.
Owner: epic-platform-sre
Wall-E Orchestration Patterns (Optum)
Patterns and guardrails for composing safe multi-agent workflows in Wall-E (Wide Array Large Language Engine), Optum's enterprise AI orchestration platform.
Owner: epic-platform-sre
security-agent-cca-fix
Run or explain Security Agent remediation through GitHub Copilot Cloud Agent from a pip-installed setup. Use when Codex needs to use --executor cca or --executor auto, create remote Copilot/CCA remediation tasks, reason about CCA budget/status, or compare local Codex execution with remote GitHub Cloud Agent execution without cloning the controller repo.
Owner: edi-security-agent
security-agent-discovery
Discover, inspect, import, refresh, and export Security Agent vulnerability data from a pip-installed setup. Use when Codex needs to list Azure Defender findings, filter by repo/severity/CVE/fixable state, refresh the local UI vulnerability cache, import Security Platform findings through explicit cookie and DPoP values, or explain discovery-only workflows without cloning the controller repo.
Owner: edi-security-agent

