Leaders Logo

Model Context Protocol (MCP): Connecting Context, Agents, and Modern Software Architecture

Introduction

Modern systems that integrate LLM-based agents and intelligent assistants into corporate services require more than simple API calls. These systems require explicitly controlled context, standardized interfaces, clear access boundaries, and formal mechanisms for governance. Scientific literature indicates that the absence of these elements results in unpredictable behavior, auditing difficulties, and security risks in AI-based systems (AMERSHI et al., 2019).

Complementing this view, this article presents a practical implementation of a MCP Server in .NET operating over a GraphQL context, specifying how to define and formalize context structures, access boundaries, and standardized interfaces. For language models (LLMs) to be robustly integrated into corporate systems, with security, auditability, and predictability, it is necessary to adopt architectures and governance mechanisms that consider access restrictions, traceability, and formal control mechanisms, avoiding unforeseen behaviors or operational insecurity (KARRAS et al., 2025). The central goal is to provide a reproducible template for real scenarios, illustrated by APIs that return results for a possible chat.

Context of the Model Context Protocol (MCP)

The MCP can be understood as an integration contract between a host (client running the model) and a server (provider of capabilities). Instead of the model “inventing” integrations, the MCP allows the host to discover and invoke explicitly defined capabilities:

  • Tools: invocable functions (e.g., querying orders via GraphQL with an allowlist).
  • Resources: read-only artifacts that provide context (e.g., schema, policies, examples).
  • Prompts: interaction templates/shortcuts (e.g., a standardized command to fetch orders).
SVG Image of the Article

In modern architectures, this pattern reduces coupling and creates a single point to apply authorization, rate limiting, auditing, and data policies, preventing the model from having direct and unrestricted access to the backend.

Definition of Context

Context, in this article, is the set of information and constraints that determine what can be accessed, how it can be accessed, and what response format is accepted. In the proposed scenario, the context is materialized by:

  • A GraphQL API as the data source for the domain.
  • A MCP Server as the governance layer, with an allowlist of operations and parameter validation.
  • Resources documenting schema, policies, and output examples.

Agents and Controlled Execution

Agents (in the functional sense) are consumers of these capabilities: the host decides whether and when to call a tool, but the server defines the boundaries of what is allowed. Thus, execution remains deterministic from the access perspective: each invocable action has a contract, validation, and auditing.

Modern Software Architecture

The implementation of the MCP benefits from microservices principles and API governance: well-defined responsibilities, security layers, observability, and decoupling. In particular, the combination of MCP + GraphQL is useful when flexibility in querying is desired, as long as it is accompanied by boundaries (complexity, depth, and allowlist).

Assumptions, Definitions, and Access Boundaries

Assumptions

  • The domain exposes a GraphQL API for reading (and optionally writing) data.
  • The MCP Server is the only authorized point to access the GraphQL API in the context of agents.
  • The model host does not receive direct credentials from the backend; it only receives the capability to invoke governed tools.

Operational Definitions

  • Tool Allowlist: a closed set of allowed operations (by name and contract).
  • Scopes: minimum permissions per tool (e.g., orders.read, orders.search).
  • Limits: timeout per call, maximum rate per client, and strict parameter validation.

Access Boundaries and Governance

The following are proposed essential access boundaries when the MCP serves as a bridge to corporate data:

  • Authentication: the client/host must present valid credentials to invoke the MCP Server.
  • Authorization: each tool validates scopes and policies (e.g., mandatory scope to query orders).
  • Allowlist of GraphQL operations: prohibit execution of arbitrary queries and only accept pre-approved operations.
  • Rate limiting: impose quotas per tool and per client (e.g., 30 req/min).
  • Timeout: cancel long queries (e.g., 3 seconds) to prevent backend degradation.
  • Auditing: log CorrelationId, invoked tool, duration, result (success/error), and minimum metadata.

Implementation of the MCP in .NET over GraphQL

In this section, a reproducible excerpt of the proposed architecture is presented, consisting of: (i) a minimal GraphQL API, (ii) a gateway with allowlist and timeout, (iii) domain tools with explicit intent, and (iv) a demonstration API capable of returning responses in chat format. The goal is to highlight how the MCP can operate over a GraphQL context securely, auditable, and predictably.

GraphQL Context: Minimal API (Hot Chocolate)

The domain context is exposed through a minimal GraphQL API, implemented with Hot Chocolate, providing only the types and queries strictly necessary for the order domain (orders). This approach reduces the attack surface, facilitates governance, and creates an explicit context contract for consumption by agents and MCP tools.

Hot Chocolate is a GraphQL framework for .NET that allows the construction of strongly typed, schema-oriented APIs integrated into the ASP.NET ecosystem (MARJANOVIC, 2022). It automates schema generation from C# types, supports validation, middlewares, efficient data resolution, and extensions such as authorization and observability, making it suitable for exposing well-defined contexts in agent-oriented architectures and MCPs.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>();

var app = builder.Build();

app.MapGraphQL("/graphql");

app.Run();

public record Order(string Id, string CustomerName, decimal Total, string Status);

public sealed class Query
{
    public IEnumerable<Order> Orders(string? status = null)
    {
        var all = new[]
        {
            new Order("ord_1001", "Ana",   120.50m, "PAID"),
            new Order("ord_1002", "Bruno",  89.90m, "PENDING"),
            new Order("ord_1003", "Carla", 450.00m, "PAID"),
        };

        return status is null
            ? all
            : all.Where(o => o.Status.Equals(status, StringComparison.OrdinalIgnoreCase));
    }

    public Order? OrderById(string id) =>
        Orders().FirstOrDefault(o => o.Id == id);
}

GraphQL Gateway with Allowlist and Timeout

The gateway encapsulates access to the GraphQL endpoint and applies explicit control policies, including operation allowlist, timeout, and cancellation. This layer prevents the execution of arbitrary queries, standardizes access to the context, and reduces risks associated with abuse, denial of service, or schema exploitation.

using System.Net.Http.Json;
using System.Text.Json;

public sealed class GraphQLGateway
{
    private readonly HttpClient _http;

    private static readonly HashSet<string> AllowedOperations =
        new(StringComparer.OrdinalIgnoreCase)
        {
            "Orders",
            "OrderById"
        };

    public GraphQLGateway(HttpClient http) => _http = http;

    public async Task<JsonElement> ExecuteAsync(
        string operationName,
        string query,
        object variables,
        CancellationToken ct)
    {
        if (!AllowedOperations.Contains(operationName))
            throw new InvalidOperationException(
                $"Operation '{operationName}' is not allowed.");

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(3));

        var payload = new
        {
            operationName,
            query,
            variables
        };

        using var resp =
            await _http.PostAsJsonAsync("/graphql", payload, cts.Token);

        resp.EnsureSuccessStatusCode();

        return await resp.Content.ReadFromJsonAsync<JsonElement>(
            cancellationToken: cts.Token);
    }
}

MCP Tools: Domain Operations (without Free GraphQL)

Instead of exposing a generic tool capable of executing any GraphQL query, intention-oriented tools are provided, each mapping to a specific business operation. This decision reduces risks, facilitates auditing, simplifies validations, and reinforces the principle of least privilege in the MCP.

using System.Text.Json;

public sealed class OrderTools
{
    private readonly GraphQLGateway _gql;

    public OrderTools(GraphQLGateway gql) => _gql = gql;

    // Tool: orders.search (suggested scope: orders.search)
    public Task<JsonElement> SearchOrdersAsync(
        string? status,
        CancellationToken ct)
    {
        const string operationName = "Orders";
        const string query = """
        query Orders($status: String) {
          orders(status: $status) {
            id
            customerName
            total
            status
          }
        }
        """; 

        var variables = new { status };
        return _gql.ExecuteAsync(operationName, query, variables, ct);
    }

    // Tool: orders.getById (suggested scope: orders.read)
    public Task<JsonElement> GetOrderByIdAsync(
        string id,
        CancellationToken ct)
    {
        const string operationName = "OrderById";
        const string query = """
        query OrderById($id: String!) {
          orderById(id: $id) {
            id
            customerName
            total
            status
          }
        }
        """; 

        var variables = new { id };
        return _gql.ExecuteAsync(operationName, query, variables, ct);
    }
}

Resources (Context) and Prompts (Flows)

Although the concrete implementation of an MCP Server varies depending on the SDK or framework used, the recommended content for resources and prompts remains stable and independent of language, serving as a basis for governance and semantic alignment between agents.

  • Resource: Schema (resource://graphql/schema): snapshot of the schema or relevant subset (orders).
  • Resource: Policies (resource://policies/access): scopes, quotas, prohibited data, and auditing rules.
  • Resource: Examples (resource://examples/orders): output examples to standardize discourse and format.
  • Prompt: /ask-orders: standardized instruction — “use only orders.search and orders.getById; do not attempt other sources; do not request sensitive data.”

Demonstration API: Chat Format Result

To illustrate the result without depending on a specific host, the following creates a REST API that returns a list of messages (role/content). This API simulates a flow: it interprets the user's text and calls governed tools.

Contract

  • POST /chat with { "text": "..." }
  • Response: { correlationId, messages: [{role, content}, ...] }
using System.Text.Json;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<GraphQLGateway>(http =>
{
    http.BaseAddress = new Uri("https://localhost:5001"); // GraphQL API
});

builder.Services.AddScoped<OrderTools>();

var app = builder.Build();

app.MapPost("/chat", async Task<Ok<ChatResponse>> (
    ChatRequest req,
    OrderTools tools,
    HttpContext http,
    CancellationToken ct) =>
{
    var correlationId = http.TraceIdentifier;

    var messages = new List<ChatMessage>
    {
        new("system", "Use only allowlisted tools. Do not return PII. Respond objectively."),
        new("user", req.Text)
    };

    if (req.Text.Contains("pago", StringComparison.OrdinalIgnoreCase) ||
        req.Text.Contains("paid", StringComparison.OrdinalIgnoreCase))
    {
        messages.Add(new("assistant", "Consulting orders with status PAID via tool orders.search..."));
        var json = await tools.SearchOrdersAsync("PAID", ct);
        messages.Add(new("assistant", SummarizeOrders(json)));
        return TypedResults.Ok(new ChatResponse(correlationId, messages));
    }

    if (req.Text.Contains("ord_", StringComparison.OrdinalIgnoreCase))
    {
        var id = ExtractOrderId(req.Text);
        messages.Add(new("assistant", $"Consulting order {id} via tool orders.getById..."));
        var json = await tools.GetOrderByIdAsync(id, ct);
        messages.Add(new("assistant", SummarizeOrder(json, id)));
        return TypedResults.Ok(new ChatResponse(correlationId, messages));
    }

    messages.Add(new("assistant",
        "Examples: \"bring me paid orders\" or \"detail order ord_1002\"."));

    return TypedResults.Ok(new ChatResponse(correlationId, messages));
});

app.Run();

static string ExtractOrderId(string text)
{
    var idx = text.IndexOf("ord_", StringComparison.OrdinalIgnoreCase);
    if (idx < 0) return "ord_1001";
    var tail = text.Substring(idx);
    var end = tail.IndexOfAny([' ', '.', ',', ';', '\n', '\r']);
    return end < 0 ? tail : tail[..end];
}

static string SummarizeOrders(JsonElement gqlJson)
{
    if (!gqlJson.TryGetProperty("data", out var data) ||
        !data.TryGetProperty("orders", out var orders))
        return "No results found.";

    var lines = new List<string> { "Orders found:" };

    foreach (var o in orders.EnumerateArray())
    {
        var id = o.GetProperty("id").GetString();
        var name = o.GetProperty("customerName").GetString();
        var total = o.GetProperty("total").GetDecimal();
        var status = o.GetProperty("status").GetString();
        lines.Add($"- {id}: {name} — {status} — Total {total:C}");
    }

    return string.Join("\n", lines);
}

static string SummarizeOrder(JsonElement gqlJson, string id)
{
    if (!gqlJson.TryGetProperty("data", out var data) ||
        !data.TryGetProperty("orderById", out var order) ||
        order.ValueKind == JsonValueKind.Null)
        return $"Order {id} not found.";

    var name = order.GetProperty("customerName").GetString();
    var total = order.GetProperty("total").GetDecimal();
    var status = order.GetProperty("status").GetString();
    return $"Order {id}: {name} — {status} — Total {total:C}";
}

public sealed record ChatRequest(string Text);
public sealed record ChatResponse(string CorrelationId, List<ChatMessage> Messages);
public sealed record ChatMessage(string Role, string Content);

Discussion: Challenges and Opportunities

Interoperability

By standardizing tools/resources/prompts, the MCP reduces coupling between the host and internal services. However, real interoperability depends on explicit contracts, versioning, deprecation, and consistent documentation (resources).

Management of Contextual Data and Security

In GraphQL, flexibility is both a benefit and a risk. Therefore, designing with an allowlist and domain tools makes access more predictable. Additionally, auditing with CorrelationId allows tracking decisions and executions, which is essential in regulated environments.

Scalability and Observability

Observability tools (structured logs, metrics, and tracing) become essential for identifying bottlenecks. As agents can increase the volume and variability of calls, quotas and caches per operation are recommended strategies to maintain predictability.

Future Directions

Future work includes: (i) adding complexity/depth policies for GraphQL, (ii) expanding the set of tools with

References

  • AMERSHI, Saleema et al. Guidelines for human-AI interaction. In: Proceedings of the 2019 CHI Conference on Human Factors in Computing Systems. 2019. p. 1-13. reference.Description
  • KARRAS, Aristeidis et al. LLM-Driven Big Data Management Across Digital Governance, Marketing, and Accounting: A Spark-Orchestrated Framework. Algorithms, v. 18, n. 12, p. 791, 2025. reference.Description
  • MARJANOVIC, Rickard. Evaluating GraphQL over REST within a .NET Web API: A controlled experiment conducted by integrating with the Swedish Companies Registration Office. 2022. reference.Description
About the author