← Back to Writing
Article· 3 min read

MCP Tools, Resources, and Prompts Explained

MCP ServerModel Context Protocol.NET AIAI EngineeringEnterprise Systems
Diagram showing MCP tools, resources, and prompts feeding an AI agent

Summary

Hands-on C# patterns for enterprise MCP servers — support ticket tools, customer profile resource URIs, documentation prompts, and contract testing with platform-domain examples.

Chapter 2 of the Hands-On MCP for C# and .NET guide introduces three MCP primitives beyond basic tools: resources (read structured data by URI), prompts (pre-built message templates), and versioning patterns. This article maps those ideas to enterprise systems — customer profiles, support tickets, inventory updates, and internal documentation.

Start with Building Your First MCP Server in C# and .NET if you have not yet exposed a catalog search tool.

MCP primitives at a glance

PrimitivePurposeEnterprise example
ToolCallable action with side effects or computationCreate support ticket, adjust inventory
ResourceRead-only data at a stable URI`customer://profile/{id}`
PromptCurated multi-turn context for the LLMOnboarding summary from CRM + docs

Tools do things. Resources expose data. Prompts shape conversations.

Tool: Create a support ticket

Tools should return strings the LLM can present directly — including business-level failures.

[McpServerToolType]
public sealed class CreateSupportTicketTool(ITicketService tickets)
{
    [McpServerTool, Description(
        "Create a support ticket for a customer issue. Returns confirmation or a descriptive failure message.")]
    public async Task<string> CreateTicketAsync(
        [Description("Customer identifier, e.g. CUST-2048.")]
        string customerId,

        [Description("Issue category: Billing, Shipping, Product, or Account.")]
        string category,

        [Description("Short summary of the issue.")]
        string summary,

        [Description("Priority: Low, Medium, or High.")]
        string priority,

        CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(summary))
            return "Ticket rejected: summary is required.";

        try
        {
            var ticket = await tickets.CreateAsync(
                customerId, category, summary, priority, ct);

            return $"Ticket created. Reference: {ticket.TicketId}. " +
                   $"Category: {category}, priority: {priority}. " +
                   $"Status: {ticket.Status}.";
        }
        catch (CustomerNotFoundException)
        {
            return $"Ticket rejected: no customer found with ID '{customerId}'.";
        }
        catch (RateLimitExceededException)
        {
            return "Ticket rejected: rate limit exceeded for this customer. Try again later.";
        }
    }
}

Design note: return descriptive strings instead of throwing to the protocol layer when the failure is expected business logic (unknown customer, validation errors). Reserve exceptions for truly exceptional cases.

Resource: Customer profile by ID

Resources address read-only data with a URI template. Hosts subscribe or fetch without pretending the operation is a mutating tool.

[McpServerResourceType]
public sealed class CustomerResourceHandler(ICustomerService customers)
{
    [McpServerResource("customer://profile/{customerId}")]
    [Description("Returns customer profile, tier, open tickets, and recent orders as JSON.")]
    public async Task<string> GetCustomerProfileAsync(
        [Description("Customer identifier.")]
        string customerId,

        CancellationToken ct = default)
    {
        var profile = await customers.GetProfileAsync(customerId, ct)
            ?? throw new InvalidOperationException(
                $"No customer found with ID '{customerId}'.");

        return JsonSerializer.Serialize(profile, new JsonSerializerOptions
        {
            WriteIndented = true
        });
    }
}

Example snapshot shape the resource might return:

{
  "customerId": "CUST-2048",
  "name": "Acme Corp",
  "tier": "Enterprise",
  "openTickets": 2,
  "recentOrders": [
    { "orderId": "ORD-9912", "sku": "WH-1000XM5", "status": "Shipped" }
  ],
  "asOf": "2026-06-16T09:30:00Z"
}

Use resources when the AI needs context without implying an action. Use tools when the AI should execute a bounded operation.

Prompt: Documentation-assisted troubleshooting

Prompts assemble system + user messages with live data injected — ideal for repeatable support and onboarding workflows.

[McpServerPromptType]
public sealed class TroubleshootingPrompt(
    ICustomerService customers,
    IDocumentationService docs)
{
    [McpServerPrompt, Description(
        "Builds a prompt asking the LLM to troubleshoot a customer issue using profile data and internal docs.")]
    public async Task<ChatMessage[]> TroubleshootAsync(
        [Description("Customer identifier.")]
        string customerId,

        [Description("Issue description from the user or agent.")]
        string issueDescription,

        CancellationToken ct = default)
    {
        var profile = await customers.GetProfileAsync(customerId, ct)
            ?? throw new InvalidOperationException(
                $"No customer found with ID '{customerId}'.");

        var relevantDocs = await docs.SearchAsync(issueDescription, limit: 5, ct);
        var docContext = string.Join("
---
",
            relevantDocs.Select(d => $"## {d.Title}
{d.Excerpt}"));

        return
        [
            new ChatMessage(ChatRole.System,
                "You are an internal support assistant. Use the customer profile and " +
                "documentation excerpts to suggest troubleshooting steps. Cite doc titles. " +
                "Do not invent product features not mentioned in the docs."),
            new ChatMessage(ChatRole.User,
                $"Customer profile:\n{JsonSerializer.Serialize(profile)}\n\n" +
                $"Issue:\n{issueDescription}\n\n" +
                $"Documentation:\n{docContext}")
        ];
    }
}

Prompts are not a substitute for human escalation — they standardize how context enters the model.

Choosing the right primitive

ScenarioUse
Fetch profile for CUST-2048Resource `customer://profile/CUST-2048`
Create support ticketTool `create_support_ticket` + approval if needed
Draft troubleshooting stepsPrompt `troubleshoot` + doc search
Answer "what is our return policy?"RAG over policy documentation

Contract testing mindset

Treat MCP tools like public API endpoints:

  • Snapshot the JSON Schema the server advertises
  • Integration-test happy path and validation failures
  • Verify error strings are safe to show end users (no stack traces, no PII from other tenants)
[Fact]
public async Task CreateTicket_Rejects_Empty_Summary()
{
    var tool = new CreateSupportTicketTool(new FakeTicketService());
    var result = await tool.CreateTicketAsync(
        "CUST-1", "Billing", "", "Medium");

    Assert.Contains("summary is required", result);
}

Common mistakes in enterprise MCP servers

  • Exposing a raw SQL or generic REST proxy tool — too much power, hard to audit
  • Using a tool for read-only customer data that should be a resource
  • Returning stack traces to the LLM on validation errors
  • Skipping idempotency keys on ticket or inventory tools (duplicate agent calls = duplicate side effects)

What I Learned Building MCP Servers

The teams I have seen succeed treat MCP primitives as API design, not AI novelty. Resources map cleanly to "GET by ID." Tools map to "POST with validation." Prompts map to "canned runbooks with live context."

The failure mode is blurring all three: a tool that returns read-only data, a resource that triggers side effects, or a prompt that tries to replace proper authorization. Pick the primitive that matches the operation semantics — your future auditors will thank you.

Next in this series

Deploying MCP Servers: Stdio, HTTP, and Approval Gates covers transport choice for Cursor vs production, plus human-in-the-loop gates before `create_support_ticket` and `adjust_inventory`.

References

Related reading