Introduction
Part I created a single-turn agent that could understand a customer message and return a safe response. Real support work needs more: the agent must read trusted business data, find the right support policy, and remember the conversation without inventing facts.
This article adds those capabilities with Microsoft Agent Framework. We will build a support agent that can look up an order and search a small Qdrant vector database. The sample keeps both tools read-only and uses local Ollama embeddings so it is easy to run and understand.
Real-World Scenario: A Pending Payment
A customer says:
My order ORD-1001 failed twice, but I see a pending charge. Is my order confirmed?A model alone cannot know the order status or the company’s pending-charge policy. The agent needs two controlled sources:
GetOrderStatusreads the order system.SearchSupportKnowledgeretrieves approved support guidance.
The model decides which tool is useful, but the application owns the tool implementation and its authorization boundary.
1. Function Tools: Let the Agent Read Business Data
What problem do tools solve?
The model understands language, but it does not automatically know the current state of an order, account, or payment. A function tool gives it a controlled way to ask the application for that information.
For the pending-payment case, the agent extracts ORD-1001 and calls get_order_status. The application reads the order system and returns Payment review and Pending charge. The model explains those facts; it does not invent them.
Customer: Is ORD-1001 confirmed?
Agent: Call get_order_status(orderId = "ORD-1001")
System: { status: "Payment review", paymentStatus: "Pending charge" }
Agent: Your order is not confirmed yet. The payment is still pending.How is a function tool built?
A function tool is a typed application function exposed to the agent. Its name, description, and parameter schema help the model decide when to request it. The function itself remains normal application code:
|
|
Use narrow inputs, validate arguments, return predictable data, and start with read-only tools. A create_refund tool would need authorization, idempotency, and human approval because it changes business state. Tool descriptions guide selection, but the host remains responsible for security.
2. MCP: Share Tools Across Applications
What problem does MCP solve?
Suppose the company already owns a payment-support service used by its web portal, call-center application, and several agents. Copying its tool code into every agent creates duplicated integrations. The Model Context Protocol (MCP) gives that service a standard way to publish tools that different hosts can discover and call.
For the same customer question, the flow can become:
Agent host -> MCP client -> Payment MCP server -> Payment system
|
+--> get_order_statusThe agent still decides that order status is needed. The MCP server owns connection details, authorization, and the actual call to the payment system. MCP changes where the capability lives and how it is discovered; it does not replace the agent, conversation memory, or RAG.
Local function tool versus MCP tool
| Boundary | Function tool | MCP tool |
|---|---|---|
| Tool owner | Same application process | Separate MCP server |
| Discovery | Registered in application code | Discovered through MCP |
| Transport | In-process call | Stdio or HTTP |
| Reuse | Mainly one host | Many hosts and agents |
| Security | Host authorization | Server authorization plus host policy |
A .NET host can discover MCP tools and pass them to an agent or chat client:
|
|
The Part II sample runs both tools in a local SupportMcpServer over stdio. The API discovers them at startup, so you can see the MCP boundary without deploying a remote service. In production, treat every discovered MCP tool as untrusted until the host applies an allow-list and authorization policy.
3. Conversation Memory: Continue the Case
What problem does memory solve?
Without conversation memory, every HTTP request looks like a new conversation. A customer would need to repeat the order number and the entire problem on every message.
Example:
Message 1: My order ORD-1001 failed twice, but I see a pending charge.
Agent: ORD-1001 is in payment review and has a pending charge.
Message 2: Should I try payment again?
Agent: No. Based on the same case and policy, avoid repeated attempts.
The API sends a sessionId with both requests. The runtime maps that ID to an AgentSession, so the framework preserves previous messages and tool results:
|
|
This is conversation memory, not a customer profile database. Long-term memory would store durable facts such as a preferred language or verified account identifier and would require its own identity, consent, retention, and access controls. The sample stores sessions in process for teaching; production APIs need durable, tenant-aware session storage.
4. RAG with Qdrant: Retrieve Approved Policy
What problem does RAG solve?
The order tool tells us what happened to ORD-1001; it does not tell us what company policy says about pending card authorizations. RAG retrieves the relevant policy passage and gives it to the model as evidence.
For this case, the retrieval flow is:
Question: "I see a pending charge. Is my order confirmed?"
|
v
Create query embedding -> Search Qdrant -> Select support passage
|
v
Model uses passage: "Pending card charges can be temporary authorization holds..."RAG has two distinct stages:
- Indexing: split documents into passages, create embeddings, and store vectors plus source metadata.
- Retrieval: embed the user question, search the vector database, apply access filters and a similarity threshold, then provide selected passages to the model.
The sample uses Qdrant for vector storage and search, and Ollama runs the local nomic-embed-text model to create 768-dimensional embeddings. This avoids requiring a second paid API while still using real semantic embeddings. The sample seeds one approved pending-charge passage when the collection is first used.
|
|
RAG is not simply “put documents in the prompt.” Retrieved text is evidence, not permission. Filter by tenant and user access, retain source metadata, defend against prompt injection in documents, and do not let retrieved text override application rules.
Connect the Agent to MCP Tools
The sample discovers both tools from SupportMcpServer and passes them to AsAIAgent:
|
|
The model may call one or both MCP tools before producing its answer. search_support_knowledge calls Ollama for an embedding and Qdrant for the matching policy passage. Tool execution should be observable and bounded. Validate orderId, limit query length, set timeouts, and return safe errors rather than leaking internal exceptions.
The Sample Application
The sample is an expanded version of Part I:
AgentToolscreates the agent and exposesAgentRuntime.AgentTools.Apihosts the HTTP endpoint.OrderStorecontains deterministic order data forORD-1001.QdrantSupportKnowledgeBasestores and retrieves approved pending-charge guidance.AgentSessionStorekeeps the conversation session in memory.
The endpoint accepts a session ID so the customer can ask a follow-up question:
|
|
The first request can use tools to answer the question. A second request with the same session ID can ask:
You:
What should I do next, and should I try payment again?The session preserves the conversation. The order tool and knowledge tool provide facts; the model turns those facts into a customer-facing answer.
Run the Sample
Prerequisites
Install the .NET 10 SDK and Docker Desktop with Docker Compose. You also need a DeepSeek API key. Compose starts Qdrant and Ollama locally and downloads the nomic-embed-text embedding model on first startup, so allow time, network access, and local disk space for the initial download.
From the sample directory:
|
|
Send the real-world request:
|
|
The response includes the session ID and generated answer. Send another request with case-1001 to continue the conversation.
The order data and seeded Qdrant passage make the sample reproducible at the application boundary. Compose starts Qdrant and Ollama, then pulls nomic-embed-text automatically. The generated wording still varies by model provider. When QDRANT_URL is omitted, the sample uses its in-memory fallback so the API and tests can run without Docker.
Testing Strategy
Unit tests verify deterministic order and retrieval behavior. Integration tests run the real API, MCP server, DeepSeek agent, Ollama embedding service, and Qdrant database. The live scenario sends the pending-payment question, verifies the response contains the known order and pending-payment facts, sends a follow-up with the same session ID, and verifies that the agent uses conversation memory to answer the retry question.
|
|
The live-provider fixture loads DS_KEY automatically. It is skipped when the key is unavailable.
Production Considerations
The order store and local embedding setup are educational. Production systems should add:
- A managed embedding service or secured local embedding deployment.
- A managed or containerized Qdrant deployment with backups and collection-level access control.
- Durable, tenant-aware session storage.
- Authentication and authorization for every session and tool call.
- Tool timeouts, retries, idempotency, and audit logs.
- Access-controlled retrieval with source metadata and citations.
- Prompt-injection defenses for documents, MCP servers, and tool results.
- Token, latency, and cost limits.
Conclusion
Tools give agents controlled access to application behavior. MCP provides a standard boundary for tools owned by another process or service. Memory preserves conversation state, while RAG supplies relevant external knowledge for the current question.
Together, these capabilities turn the Part I chat agent into a useful support assistant without giving the model unrestricted access to business systems. The next article adds Agent Harness, planning, context management, and approval-oriented execution.
Run the Complete Example
The complete runnable sample contains the API, MCP server, Qdrant setup, Ollama embedding service, DeepSeek integration, and tests for the two-turn support scenario: