
Een eerste blik op Copilot Studio
Welke coole nieuwe functies brengt Copilot Studio met zich mee?
Deze blogpost is de eerste van een reeks over het bouwen van je MCP-server, het hosten ervan op Azure en tot slot het implementeren ervan in een M365 Copilot-agent.
Het Model Context Protocol (MCP) is een open standaard, oorspronkelijk geïntroduceerd door Anthropic en nu breed geadopteerd, die definieert hoe AI-modellen communiceren met externe tools en gegevensbronnen. Zie het als een universele adapter: in plaats van dat elke AI-assistent een op maat gemaakte integratie nodig heeft voor elke API of service, biedt MCP één enkel, consistent contract.
Voor Microsoft 365 Copilot specifiek betekent MCP-ondersteuning dat je elke mogelijkheid kunt blootstellen — een database bevragen, een REST API aanroepen, vanuit SharePoint lezen, bedrijfslogica uitvoeren — als een set tools die Copilot namens de gebruiker kan ontdekken en aanroepen.
Een MCP-server stelt drie primitieven bloot:
getProjectStatus, createTicket)Voor de meeste Copilot-agentscenario’s zijn tools waar je je op zult richten.
We schrijven de server in gewoon JavaScript, geen TypeScript-compilatiestap vereist, wat het eenvoudig houdt en prima werkt met de MCP Inspector voor lokaal testen.
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod express
Update package.json om ESM-modules te gebruiken:
{
"type": "module",
"scripts": {
"dev": "node server.js",
"start": "node server.js"
}
}
Maak server.js aan in de projectroot. De belangrijkste ontwerpbeslissing hier is een enkel bestand voor beide transporten — STDIO voor lokale ontwikkeling en de MCP Inspector, HTTP voor Azure en Copilot. Eén omgevingsvariabele bepaalt welke wordt gestart:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import express from "express";
// Gegevens (vervang dit door je echte gegevensbron)
const projects = {
"PRJ-001": { name: "Intranet Redesign", status: "In Progress", owner: "Arne" },
"PRJ-002": { name: "Copilot Rollout", status: "Planning", owner: "Simon" },
"PRJ-003": { name: "Purview Governance", status: "Completed", owner: "Nico" },
};
// MCP-server en tools (gedeeld tussen beide transporten)
const server = new McpServer({ name: "project-status-server", version: "1.0.0" });
server.tool(
"getProjectStatus",
"Retrieves the current status and owner of a project by its ID.",
{ projectId: z.string().describe("The project ID, e.g. PRJ-001") },
async ({ projectId }) => {
const project = projects[projectId];
if (!project) return { content: [{ type: "text", text: `Project ${projectId} not found.` }], isError: true };
return { content: [{ type: "text", text: `**${project.name}** (${projectId})\nStatus: ${project.status}\nOwner: ${project.owner}` }] };
}
);
server.tool(
"updateProjectStatus",
"Updates the status of a project.",
{
projectId: z.string().describe("The project ID"),
status: z.enum(["Planning", "In Progress", "On Hold", "Completed"]).describe("The new status"),
},
async ({ projectId, status }) => {
if (!projects[projectId]) return { content: [{ type: "text", text: `Project ${projectId} not found.` }], isError: true };
projects[projectId].status = status;
return { content: [{ type: "text", text: `Updated ${projectId} to "${status}".` }] };
}
);
server.tool(
"listProjects",
"Returns a list of all projects with their current status.",
{},
async () => {
const list = Object.entries(projects)
.map(([id, p]) => `- **${id}** - ${p.name} (${p.status}, owner: ${p.owner})`)
.join("\n");
return { content: [{ type: "text", text: list }] };
}
);
// Transport-schakelaar
if (process.env.TRANSPORT === "stdio") {
// STDIO - voor lokale ontwikkeling en MCP Inspector
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running via STDIO");
} else {
// HTTP - voor Azure en Copilot
const app = express();
app.use(express.json());
app.get("/health", (_req, res) => res.json({ status: "ok" }));
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on("finish", () => transport.close());
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.error(`MCP server running via HTTP on http://localhost:${PORT}`));
}
Een paar dingen zijn het benadrukken waard:
process.env.TRANSPORT.console.error overal. In STDIO-modus is stdout gereserveerd voor JSON-RPC-berichten. Elke console.log corrumpeert het protocol en veroorzaakt cryptische JSON-parseerfouten in de Inspector. Gebruik altijd console.error voor logging.StreamableHTTPServerTransport van de MCP SDK verwacht een vooraf geparseerde aanvraagbody. Express verwerkt dit standaard netjes.Start de server in HTTP-modus:
node server.js
Het MCP-transport gebruikt Server-Sent Events (SSE), dus er zijn een paar dingen die je goed moet zetten:
-N (geen buffering) mee, anders blijft curl stilAccept: application/json, text/event-stream toe, zonder dit krijg je een 406 Not Acceptablecurl -N -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}"
Een succesvolle reactie ziet er als volgt uit:

De MCP Inspector geeft je een goede browser-UI om tools interactief te bekijken en aan te roepen.
npx @modelcontextprotocol/inspector
Configureer de verbinding in de Inspector-UI:
nodeserver.jsTRANSPORT = stdioKlik op Connect en je ziet alle drie de tools klaarstaan.
Caution
De Inspector spawnt je server zelf via STDIO, start de server dus niet afzonderlijk voordat je verbinding maakt. Zorg er ook voor dat TRANSPORT=stdio is ingesteld in de omgevingsvariabelen, anders start de server in HTTP-modus en kan de Inspector er niet mee communiceren.

Een succesvolle reactie:
