This article details the architectural decisions and security considerations for building a Model Context Protocol (MCP) server on AWS to integrate LLM clients with a B2B intelligence platform managing over one million company profiles. The core focus is on treating the MCP server as a first-class production interface with strict contracts, separating read and write operations, and robust validation to ensure safety and scalability with real business data.
Read original on InfoQ CloudThe article describes the design and implementation of an MCP server to enable an LLM client to interact with a B2B intelligence platform. The primary challenge was to create a safe and scalable bridge between the LLM and sensitive production data, rather than a simple API wrapper. This involved treating the MCP server as a first-class interface with its own contracts, security assumptions, and operational controls. The underlying platform, built on AWS AppSync, stores over one million company profiles, necessitating a design that prioritizes scale, narrow tool boundaries, predictable request handling, and clear auditability.
The request flow from an LLM client to the backend involves several distinct layers, ensuring robust validation and secure execution:
Key Isolation Principle
A critical design choice is that the MCP server never passes raw user input directly to GraphQL, nor does it return raw GraphQL responses to the client. This isolation minimizes the risk of prompt-level ambiguity affecting backend behavior directly.
One of the most crucial architectural decisions was to rigorously separate read and write operations from the outset. Unlike many prototype examples, where tools might combine search, update, and orchestration, this system explicitly divides these functionalities. Read paths are strictly read-only, and mutation-capable actions are blocked by default via an `allowMutations` flag at the tool registry level. This design significantly enhances safety, simplifies testing, and improves auditability by ensuring tools express a single, clear intent.
func NewRegistry(gqlClient graphql.Client, allowMutations bool) *Registry {
return &Registry{
gqlClient: gqlClient,
// Read-only tools – no mutation flag needed
searchCompanies: NewSearchCompaniesTool(gqlClient),
// ... other read tools
// Mutation tools – receive the flag
createCollection: NewCreateCollectionTool(gqlClient, allowMutations),
addToCollection: NewAddToCollectionTool(gqlClient, allowMutations),
// ... other mutation tools
}
}
func (t *CreateCollectionTool) Execute(ctx context.Context, params CreateCollectionParams) (*CreateCollectionResult, error) {
if !t.mutationsAllowed {
return nil, fmt.Errorf("mutations are disabled; use --allow-mutations flag to enable write operations")
}
// ... validation and execution follow
}The implementation defined nine tools, categorized into read-only (e.g., `search_companies`, `get_company`) and mutation-capable (e.g., `create_collection`, `add_to_collection`, `request_email_discovery`). Critical insights from integration testing led to disabling `create_collection` in production due to a backend Lambda null-pointer error not caught by unit tests alone. This highlights the importance of real-system validation as a release gate, even with comprehensive mocked tests.