07-03-02 — Desafio: MCP Server para API SharePoint
TL;DR
Desafio prático: construa um MCP server que expõe duas tools — list_sharepoint_sites e search_documents — usando a Microsoft Graph API. O resultado: qualquer host MCP (Claude Desktop, VS Code Copilot, Copilot Studio) pode buscar documentos corporativos do SharePoint diretamente no chat, sem configuração adicional. Código completo em Python com autenticação via Entra ID.
O desafio
Sua missão: criar um MCP server que permita ao Copilot do VS Code responder perguntas como "Qual a versão atual do manual de onboarding no SharePoint?" ou "Liste todos os documentos de arquitetura aprovados nos últimos 30 dias."
Tecnologias: Python, Microsoft Graph API, MSAL (autenticação Entra ID), MCP SDK.
Pré-requisitos: App Registration no Entra ID
Antes do código, você precisa registrar uma aplicação no Azure Entra ID com as permissões corretas:
- Azure Portal → Entra ID → App registrations → New registration
- Nome:
sharepoint-mcp-server - Em "API permissions", adicione:
Sites.Read.All,Files.Read.All(Microsoft Graph, Application permissions) - Grant admin consent
- Em "Certificates & secrets", crie um client secret
- Anote:
tenant_id,client_id,client_secret
O server MCP roda como processo de background, sem usuário logado. Por isso usamos Application permissions (Sites.Read.All), não Delegated. Isso significa que o server tem acesso a todos os sites — implemente filtros se necessário para limitar o escopo a sites específicos.
Instalação
pip install mcp msal httpx python-dotenv
Código completo: sharepoint_mcp_server.py
import os
import httpx
import msal
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from dotenv import load_dotenv
import json
from datetime import datetime, timedelta
load_dotenv()
# ---- CONFIGURAÇÃO ----
TENANT_ID = os.environ["ENTRA_TENANT_ID"]
CLIENT_ID = os.environ["ENTRA_CLIENT_ID"]
CLIENT_SECRET = os.environ["ENTRA_CLIENT_SECRET"]
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
# ---- AUTENTICAÇÃO ----
_token_cache: dict = {}
def get_access_token() -> str:
"""Obtém token MSAL com cache simples."""
now = datetime.utcnow()
if _token_cache.get("expires_at") and now < _token_cache["expires_at"]:
return _token_cache["token"]
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
client_credential=CLIENT_SECRET
)
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
if "access_token" not in result:
raise RuntimeError(f"Falha na autenticação: {result.get('error_description')}")
_token_cache["token"] = result["access_token"]
_token_cache["expires_at"] = now + timedelta(seconds=result.get("expires_in", 3600) - 60)
return _token_cache["token"]
def graph_headers() -> dict:
return {
"Authorization": f"Bearer {get_access_token()}",
"Accept": "application/json"
}
# ---- MCP SERVER ----
server = Server("sharepoint-mcp")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="list_sharepoint_sites",
description=(
"Lista sites do SharePoint da organização. "
"Use para descobrir quais sites existem antes de buscar documentos."
),
inputSchema={
"type": "object",
"properties": {
"search": {
"type": "string",
"description": "Filtro opcional pelo nome do site (ex: 'RH', 'TI', 'Projetos')"
}
}
}
),
Tool(
name="search_documents",
description=(
"Busca documentos no SharePoint por palavras-chave. "
"Retorna título, URL, autor e data de modificação. "
"Use para encontrar documentos específicos por conteúdo ou nome."
),
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Termo de busca (ex: 'manual onboarding', 'arquitetura microsserviços')"
},
"site_id": {
"type": "string",
"description": "ID do site para limitar a busca (opcional). Use list_sharepoint_sites para obter IDs."
},
"limit": {
"type": "integer",
"description": "Número máximo de resultados (padrão: 10, máximo: 25)",
"default": 10
}
},
"required": ["query"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "list_sharepoint_sites":
return await list_sites(arguments.get("search"))
elif name == "search_documents":
return await search_docs(
query=arguments["query"],
site_id=arguments.get("site_id"),
limit=min(arguments.get("limit", 10), 25)
)
raise ValueError(f"Tool desconhecida: {name}")
async def list_sites(search: str | None = None) -> list[TextContent]:
"""Lista sites SharePoint via Graph API."""
url = f"{GRAPH_BASE}/sites"
params = {"$top": 20, "$select": "id,displayName,webUrl,description"}
if search:
params["$search"] = f'"{search}"'
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers=graph_headers(), params=params)
resp.raise_for_status()
data = resp.json()
sites = data.get("value", [])
if not sites:
return [TextContent(type="text", text="Nenhum site encontrado.")]
lines = ["**Sites SharePoint encontrados:**\n"]
for site in sites:
lines.append(f"- **{site['displayName']}**")
lines.append(f" ID: `{site['id']}`")
lines.append(f" URL: {site.get('webUrl', 'N/A')}")
if site.get("description"):
lines.append(f" Descrição: {site['description'][:100]}")
lines.append("")
return [TextContent(type="text", text="\n".join(lines))]
async def search_docs(query: str, site_id: str | None, limit: int) -> list[TextContent]:
"""Busca documentos via Graph Search API."""
search_body = {
"requests": [{
"entityTypes": ["driveItem"],
"query": {"queryString": query},
"from": 0,
"size": limit,
"fields": ["name", "webUrl", "lastModifiedDateTime", "createdBy", "size", "parentReference"]
}]
}
# Adicionar filtro de site se especificado
if site_id:
search_body["requests"][0]["query"]["queryString"] += f" site:{site_id}"
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{GRAPH_BASE}/search/query",
headers=graph_headers(),
json=search_body
)
resp.raise_for_status()
data = resp.json()
hits = []
for response in data.get("value", []):
for result in response.get("hitsContainers", []):
hits.extend(result.get("hits", []))
if not hits:
return [TextContent(type="text", text=f"Nenhum documento encontrado para: '{query}'")]
lines = [f"**Documentos encontrados para '{query}':**\n"]
for hit in hits:
resource = hit.get("resource", {})
name = resource.get("name", "Sem nome")
url = resource.get("webUrl", "#")
modified = resource.get("lastModifiedDateTime", "")[:10] if resource.get("lastModifiedDateTime") else "?"
author = resource.get("createdBy", {}).get("user", {}).get("displayName", "Desconhecido")
lines.append(f"- **{name}**")
lines.append(f" URL: {url}")
lines.append(f" Modificado: {modified} | Autor: {author}")
lines.append("")
return [TextContent(type="text", text="\n".join(lines))]
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(server))
Arquivo .env
ENTRA_TENANT_ID=seu-tenant-id-aqui
ENTRA_CLIENT_ID=seu-client-id-aqui
ENTRA_CLIENT_SECRET=seu-client-secret-aqui
Configurando no VS Code
// .vscode/settings.json
{
"github.copilot.chat.mcpServers": {
"sharepoint": {
"command": "python",
"args": ["${workspaceFolder}/sharepoint_mcp_server.py"],
"env": {
"ENTRA_TENANT_ID": "${env:ENTRA_TENANT_ID}",
"ENTRA_CLIENT_ID": "${env:ENTRA_CLIENT_ID}",
"ENTRA_CLIENT_SECRET": "${env:ENTRA_CLIENT_SECRET}"
}
}
}
}
Testando no Claude Desktop
// claude_desktop_config.json
{
"mcpServers": {
"sharepoint": {
"command": "python",
"args": ["C:/projetos/sharepoint_mcp_server.py"],
"env": {
"ENTRA_TENANT_ID": "seu-tenant-id",
"ENTRA_CLIENT_ID": "seu-client-id",
"ENTRA_CLIENT_SECRET": "seu-secret"
}
}
}
}
Após configurar, você pode perguntar diretamente no chat: "Liste os sites do SharePoint relacionados a RH" ou "Busque documentos sobre arquitetura de microsserviços no SharePoint".
A Microsoft disponibilizou um MCP server oficial para Microsoft 365 (inclui SharePoint) em github.com/microsoft/Microsoft365-MCP-Server. O server customizado deste desafio é útil quando você precisa de lógica de negócio específica ou acesso a metadados customizados do SharePoint.
Próximos passos sugeridos
- Adicionar tool
get_document_contentque lê o conteúdo de um arquivo via Graph API - Hospedar como Azure Function (ver 07-01-03) para acesso remoto por toda a equipe
- Adicionar cache Redis para reduzir chamadas à Graph API
- Implementar autenticação via Managed Identity ao invés de client secret