Menu
InfoQ Cloud·May 18, 2026

Designing a Secure LLM-Integrated B2B Platform on AWS

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 Cloud

Introduction to LLM-Integrated B2B Platform Architecture

The 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.

Core Architectural Principles

  • First-class Interface: The MCP server is not a thin wrapper but a dedicated interface with its own design considerations.
  • Narrow Tool Boundaries: Business logic is kept within narrowly scoped tools rather than pushed into the LLM layer, making the system easier to test and reason about.
  • Separation of Concerns: AppSync remains the system of record for backend access, preventing the MCP layer from becoming an ad hoc integration point.
  • Contract Enforcement: The MCP server acts as a contract-enforcing layer, normalizing user requests into explicit tool calls and ensuring consistent response shapes.

End-to-End Request Flow

The request flow from an LLM client to the backend involves several distinct layers, ensuring robust validation and secure execution:

  1. LLM Client Tool Call: Sends a JSON argument payload over the MCP stdio transport.
  2. MCP Server Dispatch: The `mcp-go` library dispatches the call to the registered tool handler.
  3. Argument Parsing and Validation: Tool handlers parse arguments into typed Go structs, perform required field checks, enforce limits (e.g., max 100 results), trim/normalize inputs, and check `mutationsAllowed` for write operations. Requests fail immediately if validation fails.
  4. GraphQL Execution: The tool constructs GraphQL variables and executes against the AppSync endpoint, with the GraphQL client handling authentication (OIDC, API key, AWS SigV4) and HTTP errors.
  5. Response Shaping: GraphQL responses are unmarshaled into internal types and then mapped to flat, AI-friendly public types, ensuring consistent output for the LLM.
  6. Serialization and Return: The result is serialized to JSON and returned to the LLM client as a `CallToolResult`.
💡

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.

Strict Separation of Read and Write Operations

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.

go
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
}

Tool Inventory and Production Considerations

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.

LLM IntegrationAWS AppSyncGraphQLB2B PlatformSecurity Best PracticesAPI GatewayData ValidationMicroservices

Comments

Loading comments...