cum construiești un server mcp remote pentru microsoft graph în .net

Cum construiești un server MCP remote pentru Microsoft Graph în .NET

6/16/2026

Cum construiești un server MCP remote pentru Microsoft Graph în .NET

Majoritatea demo-urilor „AI pentru Microsoft 365” se destramă în momentul în care cineva cere asistentului să facă ceva real: să găsească persoana potrivită în director, să verifice un calendar, să deschidă o cerere. Modelul e capabil. Doar că nu are o cale sigură și guvernată de a ajunge la tenantul tău. Acel gol e întreaga problemă și exact pe el îl închide un server Model Context Protocol (MCP).

Așa construiesc un server MCP remote care expune Microsoft Graph către un client AI, în C# și ASP.NET Core. Mă concentrez pe părțile care contează în producție și pe cele peste care ghidurile rapide trec ușor: cele două granițe de autentificare, alegerea transportului și acele câteva decizii care separă un demo de ceva ce poți pune în fața unui tenant.

Ce este de fapt un server MCP

MCP este un protocol deschis pentru conectarea modelelor AI la instrumente și date externe. Un server expune un set de instrumente (funcții pe care modelul le poate apela) și resurse (date pe care le poate citi); un client — Claude, Copilot Studio, un IDE, un agent personalizat — descoperă acele instrumente și le apelează în numele modelului.

„Remote” contează aici. Un server MCP local rulează pe mașina ta prin stdio și servește un singur client. Un server MCP remote rulează ca serviciu HTTP, servește mulți clienți și stă lângă restul infrastructurii tale. Dacă vrei ca un agent Copilot sau asistenții unei echipe să ajungă la Graph, vrei remote.

Arhitectura, în trei cutii

Sunt trei componente și, important, două granițe de autentificare:

  • Clientul AI vorbește cu serverul tău MCP prin HTTP. (Granița 1: cine are voie să-ți apeleze serverul.)
  • Serverul tău MCP vorbește cu Microsoft Graph. (Granița 2: ce are voie serverul tău să facă în tenant.)

Lumea se fixează pe a doua graniță și o uită pe prima. Un server care poate acționa pe Graph și e accesibil pe internet fără autentificare nu e o funcționalitate; e un incident care așteaptă să se întâmple. Tratează ambele granițe ca fiind esențiale.

Autentificarea, partea care mușcă

Începe în Microsoft Entra ID. Înregistrează o aplicație și alege între două modele de permisiuni:

  • Permisiuni delegate: serverul acționează ca utilizatorul autentificat, moștenind accesul lui. Corect când fiecare cerere are context real de utilizator.
  • Permisiuni de aplicație: serverul acționează ca el însuși, independent de orice utilizator. Mai simplu de cablat pentru un serviciu de backend, dar raza de impact e tot scope-ul de tenant pe care l-ai acordat. Alege cea mai îngustă permisiune care rezolvă treaba și obține consimțământul de admin.

Pentru un server app-only, Azure.Identity împreună cu SDK-ul Microsoft Graph e calea curată. ClientSecretCredential te pune pe drum; în Azure, preferă o managed identity ca să nu existe niciun secret:

using Azure.Identity;
using Microsoft.Graph;

// ClientSecretCredential e bun pentru început; în producție preferă un certificat sau o managed identity.
var credential = new ClientSecretCredential(
    tenantId: Environment.GetEnvironmentVariable("AAD_TENANT_ID"),
    clientId: Environment.GetEnvironmentVariable("AAD_CLIENT_ID"),
    clientSecret: Environment.GetEnvironmentVariable("AAD_CLIENT_SECRET"));

var graph = new GraphServiceClient(
    credential,
    scopes: new[] { "https://graph.microsoft.com/.default" });

O observație care îți va salva o alertă la 3 dimineața: un client secret e o parolă într-o variabilă de mediu. Rotește-l sau, mai bine, folosește un certificat sau o managed identity, ca să nu existe niciun secret de scurs. DefaultAzureCredential îți permite să schimbi un secret local cu o managed identity în Azure fără să modifici codul.

Un instrument Graph real

În SDK-ul C# un instrument e pur și simplu o metodă, marcată cu atribute. Iată unul util: caută persoane în director. Observă două lucruri — modelează răspunsul (nu-i da niciodată modelului payload-ul Graph brut; risipește tokeni și încurcă modelul), iar atributele [Description] sunt ceea ce citește modelul ca să decidă când și cum apelează instrumentul.

using System.ComponentModel;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using ModelContextProtocol.Server;

[McpServerToolType]
public class DirectoryTools
{
    private readonly GraphServiceClient _graph;
    public DirectoryTools(GraphServiceClient graph) => _graph = graph;

    [McpServerTool, Description("Search the directory for people by display name. Returns name, email, and job title.")]
    public async Task<object> FindUsers(
        [Description("Part of the person's display name")] string query)
    {
        var result = await _graph.Users.GetAsync(req =>
        {
            req.QueryParameters.Search = 
quot;\"displayName:{query}\""; req.QueryParameters.Select = new[] { "id", "displayName", "mail", "jobTitle" }; req.QueryParameters.Top = 10; req.Headers.Add("ConsistencyLevel", "eventual"); }); // Modelează rezultatul — nu returna niciodată payload-ul Graph brut către model. return (result?.Value ?? new List<User>()) .Select(u => new { name = u.DisplayName, email = u.Mail, title = u.JobTitle }) .ToArray(); } }

Header-ul ConsistencyLevel: eventual e necesar pentru $search pe endpoint-ul de users — unul dintre acele detalii pe care le afli abia după primul 400.

Cablarea serverului MCP

Acum serverul în sine, ca aplicație ASP.NET Core. Clientul Graph intră în DI ca instrumentele să-l ia prin constructor; serverul MCP e înregistrat cu transportul HTTP, iar MapMcp expune endpoint-ul.

using Azure.Identity;
using Microsoft.Graph;

var builder = WebApplication.CreateBuilder(args);

// Clientul Graph, injectat în instrumente.
builder.Services.AddSingleton(_ =>
{
    var credential = new ClientSecretCredential(
        Environment.GetEnvironmentVariable("AAD_TENANT_ID"),
        Environment.GetEnvironmentVariable("AAD_CLIENT_ID"),
        Environment.GetEnvironmentVariable("AAD_CLIENT_SECRET"));
    return new GraphServiceClient(credential, new[] { "https://graph.microsoft.com/.default" });
});

// Server MCP peste Streamable HTTP, instrumente descoperite din assembly.
builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp();   // expune endpoint-ul MCP peste Streamable HTTP
app.Run();

Două lucruri merită subliniate. Întâi, SDK-urile MCP se mișcă repede — verifică numele pachetelor și metodelor față de versiunea pe care o instalezi (ModelContextProtocol și ModelContextProtocol.AspNetCore la momentul scrierii, ambele în preview). Apoi, WithHttpTransport îți dă Streamable HTTP, transportul actual, și gestionează durata sesiunii pentru tine — nu mai vechiul HTTP+SSE, care e deprecat. A recurge la SSE din obișnuință e cea mai frecventă greșeală pe care o văd.

Un câștig discret al stack-ului .NET: SDK-ul Graph vine cu un retry handler care respectă deja Retry-After de la Graph la throttling. Tot ar trebui să te aștepți la 429-uri sub sarcină, dar nu trebuie să-ți scrii singur backoff-ul.

Conectarea unui client

Un client se conectează la server prin Streamable HTTP. Forma diferă de la client la client, dar conceptual:

{
  "mcpServers": {
    "graph": {
      "type": "streamable-http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer <token emis de autentificarea serverului TĂU>" }
    }
  }
}

Acel token bearer e granița 1 de mai devreme — clientul care se autentifică la serverul tău. Specificația MCP definește un flux OAuth complet pentru asta; cel puțin, pune serverul în spatele unui proxy sau gateway care autentifică, ca să nu fie niciodată accesibil anonim.

Ce sar ghidurile rapide

Aici stă munca reală și aici petrec cel mai mult timp în proiectele cu clienți:

  • Două granițe de autentificare, nu una. Blochează cine poate apela serverul, nu doar ce poate face serverul în Graph.
  • Privilegiu minim per instrument. Permisiunile de aplicație sunt grosiere. Dacă un instrument trebuie doar să citească utilizatori, nu lăsa aplicația să scrie mail. Unde permisiunile protocolului sunt boante, impune propriul allow-list în fiecare instrument.
  • Modelează fiecare răspuns. Taie payload-urile Graph la câmpurile de care are nevoie modelul. Rezultate mai mici costă mai puțini tokeni și produc răspunsuri mai bune.
  • Așteaptă-te la intrări greșite. Atributele [Description] și tipurile parametrilor sunt schema ta; returnează text de eroare util când ceva nu e în regulă. Un mesaj de eroare bun e un prompt — modelul îl citește și reîncearcă corect.
  • Throttling-ul tot apare. Retry handler-ul SDK-ului ajută, dar proiectează instrumentele să eșueze grațios când Graph te respinge sub sarcină susținută.
  • Loghează ce a făcut agentul. Când un asistent poate acționa pe un tenant, un traseu de audit cu ce instrument a rulat, cu ce argumente, pentru ce apelant, nu e opțional.
  • Elimină secretul permanent. Certificat sau managed identity în locul unui client secret într-o variabilă de mediu.

Unde duce asta

Modelul nu se oprește la Graph. Aceeași formă — un server remote autentificat, instrumente care învelesc un API real, răspunsuri tăiate pentru un model — e modul în care conectezi un asistent la ServiceNow, la o bază de date internă sau la un API de business. Graph e pur și simplu cel mai valoros loc de unde să începi într-o organizație Microsoft, pentru că atât de mult din ce vor oamenii să facă un asistent stă deja acolo.

Pune cele două granițe la punct, ține instrumentele înguste și bine descrise, și ajungi la ceva cu adevărat util: un asistent care poate face muncă reală în tenantul tău, în siguranță. Ăsta e tot scopul — și e departe de un demo.

Dacă te gândești la o integrare MCP sau Copilot pentru propriile sisteme, asta e munca pe care o fac. Partea grea e rareori modelul; e instalația și guvernanța din jurul lui.

MCPMicrosoft Graph.NETC#agenți AI