Microsoft Agent Framework — Egroo Integration
When to Use
- Creating or modifying an
AgentDefinitionand its associated CRUD endpoints - Adding a new function tool (builtin or MCP-backed)
- Wiring a new LLM provider (OpenAI, Azure OpenAI, Anthropic, Ollama)
- Working with agent conversations, session state, or message history
- Implementing or debugging streaming chat (
ChatStreamAsync) - Adding knowledge items injected into the system prompt
- Integrating a new MCP server for tool discovery/invocation
Key Files
| File | Purpose |
|---|---|
src/Egroo.Server/Services/AgentRuntimeService.cs |
Core: builds AIAgent from AgentDefinition, runs chat turns, manages history |
src/Egroo.Server/Services/BuiltinTools.cs |
Predefined AIFunction tools (datetime, timezone) — add new builtins here |
src/Egroo.Server/Services/McpClientService.cs |
JSON-RPC HTTP client for MCP tool discovery and invocation |
src/Egroo.Server/API/AgentEndpoint.cs |
All /api/v1/Agent Minimal API routes |
src/jihadkhawaja.chat.shared/Models/Agent.cs |
AgentDefinition, AgentTool, AgentKnowledge, AgentConversation, AgentConversationMessage, LlmProvider enum |
src/jihadkhawaja.chat.shared/Interfaces/IAgentRepository.cs |
Repository interface for all agent CRUD operations |
src/Egroo.Server/Repository/AgentRepository.cs |
EF Core implementation of IAgentRepository |
src/jihadkhawaja.chat.client/Services/AgentService.cs |
Client-side HTTP service (mirrors server endpoints) |
For framework details, see ./references/ms-agent-framework.md.
Architecture
Client (Blazor)
└─ AgentService (HTTP)
└─ POST /api/v1/Agent/conversations/{id}/send-message
└─ AgentRuntimeService.ChatAsync / ChatStreamAsync
├─ IAgentRepository → loads AgentDefinition, knowledge, tools, history
├─ EncryptionService → decrypts agent API key
├─ AIAgent (Microsoft.Agents.AI)
│ └─ provider: OpenAI | AzureOpenAI | Anthropic | Ollama
├─ BuiltinTools → pre-built AIFunctions
└─ McpClientService → dynamic MCP tool proxies
Procedure
1. Creating a New Agent (Backend)
Model:
AgentDefinition(injihadkhawaja.chat.shared/Models/Agent.cs) holds all config. Required fields:UserId,Name,Provider(LlmProviderenum),Model,ApiKey(stored AES-encrypted),Instructions.Encrypt the API key before saving —
EncryptionService.Encrypt(apiKey). Keys are stripped from all API responses.Register via
POST /api/v1/Agent— already wired inAgentEndpoint.cs.Repository: All CRUD goes through
IAgentRepository→AgentRepository. Register as scoped.
2. Adding a Builtin Function Tool
Add to src/Egroo.Server/Services/BuiltinTools.cs:
public static AIFunction MyNewTool { get; } = AIFunctionFactory.Create(
([Description("The input")] string input) =>
{
// ... logic ...
return Task.FromResult("result");
},
name: "my_new_tool",
description: "What this tool does.");
Then include it in AgentRuntimeService alongside existing builtins:
var tools = new List<AIFunction> { BuiltinTools.GetCurrentDatetime, BuiltinTools.MyNewTool };
Seed it for an agent via POST /api/v1/Agent/{agentId}/seed-builtin-tools.
3. Wiring a New LLM Provider
AgentRuntimeService switches on AgentDefinition.Provider. Add a new case:
LlmProvider.Ollama => new OllamaApiClient(new Uri(definition.Endpoint!))
.AsChatClient(definition.Model)
.AsAIAgent(instructions: systemPrompt, tools: tools),
Available NuGet packages already in Egroo.Server.csproj:
Microsoft.Agents.AI.OpenAI—.GetChatClient(model).AsAIAgent(...)Microsoft.Agents.AI.Anthropic—AnthropicClient.AsAIAgent(...)Microsoft.Extensions.AI.Ollama—OllamaApiClient.AsChatClient(model).AsAIAgent(...)Azure.AI.OpenAI—AzureOpenAIClient.GetChatClient(model).AsAIAgent(...)
4. Adding Knowledge Items
Knowledge is injected into the system prompt by AgentRuntimeService:
var knowledgeItems = await _agentRepo.GetAgentKnowledgeAsync(agentId);
var systemPrompt = definition.Instructions + "\n\n" +
string.Join("\n", knowledgeItems.Where(k => k.IsEnabled).Select(k => k.Content));
Add via POST /api/v1/Agent/{agentId}/knowledge.
5. Connecting an MCP Server
- Register:
POST /api/v1/Agent/{agentId}/mcp-serverswith endpoint + optional API key. - Discover tools:
POST /api/v1/Agent/{agentId}/mcp-servers/discover-tools— callsMcpClientService.DiscoverToolsAsyncand persists toAgentTooltable withSource = Mcp. - At runtime,
AgentRuntimeServicewraps each MCP tool as anAIFunctionthat proxies throughMcpClientService.CallToolAsync.
McpClientService uses standard JSON-RPC 2.0 (tools/list, tools/call). API keys are sent as Authorization: Bearer headers.
6. Running a Conversation Turn
// Non-streaming
var response = await _agentRuntimeService.ChatAsync(userId, conversationId, "Hello");
// Streaming (IAsyncEnumerable<string>)
await foreach (var token in _agentRuntimeService.ChatStreamAsync(userId, conversationId, "Hello"))
{
// push token to client via SignalR or SSE
}
Session state and message history are persisted per AgentConversation.Id via IAgentRepository.
7. Adding a New API Endpoint
Follow the Minimal API group pattern in AgentEndpoint.cs:
group.MapPost("/{agentId}/my-feature", async (IAgentRepository repo, Guid agentId, MyRequest req, HttpContext ctx) =>
{
// use repo, return Results.Ok/NotFound
})
.RequireAuthorization();
All agent routes require JWT auth (RequireAuthorization()) and inherit the "Api" rate limit policy.
Security Checklist
- API keys are always encrypted with
EncryptionServicebefore DB storage - API keys are stripped from all responses (check
AgentEndpoint.csresponse mapping) - MCP server endpoints should be validated/allowlisted before calling in production
-
GetConnectorUserId()fromBaseRepositoryis used to scope agents to the authenticated user - Never log decrypted API keys
Common Pitfalls
LlmProvider.Ollamarequires anEndpointfield onAgentDefinition— validate it non-null before calling.- Conversation
SessionStateis stored as serialized JSON in the DB — full history is loaded on every turn. For long conversations, consider truncation. - MCP tool discover is manual — tools are not auto-refreshed; call the discover endpoint after updating an MCP server.
jihadkhawaja.chat.sharedchanges require rebuilding shared → server/client in order before tests pass.- Streaming endpoint must be called with
HttpCompletionOption.ResponseHeadersReadfrom the client.