MCP Servers and M365 (Part 1): Deploy a Microsoft 365 Roadmap MCP Server

Part 1 uses the Microsoft 365 Roadmap MCP Server to introduce MCP fundamentals by deploying it locally: run and test a no-auth TypeScript MCP wrapper over the public M365 Roadmap API, with tool input schemas generated from OpenAPI 3.0 as the source of truth.

This series starts intentionally simple: one MCP server, no authentication, and one very focused capability—wrap the Microsoft 365 Roadmap public API so AI clients can call it as a tool.

But in this series you’re not building an MCP server from scratch first. You’re going to deploy an MCP server locally, then use it as a hands-on way to learn MCP’s basic architecture by running and testing the server end-to-end.

TL;DR: Skip down to the Try it yourself section to get started quickly.

If you’re coming to MCP for the first time, it can look like more work than just calling an API directly—and that reaction is fair.

Why MCP feels more complex

  • Abstraction layer: you define a capability as a tool, and then invoke it via tools/call. That’s extra steps compared to “make an HTTP request.”
  • Serialization & schema: tools need explicit input/output contracts (JSON Schema). That’s upfront work, and mismatches can be frustrating.
  • Context management: MCP is designed for agents that may chain multiple tools over multiple turns. That power adds structure (and therefore complexity).

Why it’s done this way

  • Standardization: MCP gives AI clients one consistent interface for many different tools, so they can discover and reason about capabilities in a uniform way.
  • Safety & validation: schemas let the server reject invalid inputs early and behave predictably.
  • Extensibility: once a tool is defined, it can be reused across models and workflows without rewriting the underlying API logic.

Is it more error-prone?

Initially, it can be—because you’re translating an API into a structured tool definition, and misalignment in schema, naming, or parameter handling can cause issues.

But in larger systems, MCP usually reduces long-term errors by enforcing consistency, making inputs explicit, and enabling better orchestration and troubleshooting.

The goal of Part 1 is to make that tradeoff concrete. By running a simple, no-auth server locally, you can see exactly where the “ceremony” is (schemas, validation, routing) and why it becomes valuable before you add enterprise concerns like authentication.

MCP (Model Context Protocol) is a standard for exposing tools to AI agents and other AI clients. The practical benefit is that you can publish a standard “tool interface” (names + JSON schemas) while your server handles the complex parts: validation, query construction, and talking to upstream APIs.

Important scope note: we are not adding authentication to this Roadmap server. Later in the series we’ll introduce auth (and the “real” enterprise token handling story) using a different server: https://github.com/mjfusa/mcp-message-center-server.

Microsoft 365 Roadmap MCP Server

The first server in the series is the Microsoft 365 Roadmap MCP Server. You’ll run it locally as-is, then use it to understand the moving parts of MCP.

It provides an MCP wrapper around:

The Roadmap site publishes estimated release dates and descriptions for Microsoft 365 features. The API is the “machine interface” behind that data, and it supports OData-style query parameters, which makes it ideal for a tool wrapper.

This server is “public-only”:

  • Queries the public Roadmap API (not Microsoft Graph)
  • Requires no Microsoft Entra app registration and no OAuth tokens
  • Passes OData parameters through (no extra proprietary filtering logic)

MCP Standard Calls

The server supports the following MCP standard calls:

  • initialize: initializes the MCP session.
  • tools/list: lists available tools (here, just getM365RoadmapInfo)
  • tools/call: invokes a tool with validated arguments

These are called via HTTP POST /mcp with JSON-RPC 2.0 payloads.

What does the server expose?

This server exposes:

  • Health check endpoint: GET /healthz
  • MCP JSON-RPC endpoint: POST /mcp
  • One MCP tool: getM365RoadmapInfo
    • Supports OData parameters: filter, orderby, top, skip, count

The novel approach: OpenAPI 3.0 as the source of truth

Most MCP examples hand-write tool schemas. That’s fine for demos, but it breaks down as soon as:

  • you add endpoints,
  • you evolve parameters,
  • or the upstream API changes.

The approach in this series is: use OpenAPI 3.0 as the source of truth, and generate MCP tool schemas in code.

That means your server treats the OpenAPI definition as the authoritative contract, then derives:

  • tool names and descriptions (from operationId / summary)
  • JSON Schema for tool inputs (from parameters + request bodies)
  • validation rules (required vs optional inputs, types, enums)

Querying the Roadmap API (OData parameters)

The Roadmap API supports OData-style query parameters such as:

  • $filter (boolean expression)
  • $select (projection)
  • $orderby (sorting)
  • $top (limit)
  • $skip (pagination)
  • $count (return total count)

Here is the openapi.json file used in this server: https://raw.githubusercontent.com/mjfusa/mcp-roadmap-server/refs/heads/main/openapi/openapi.json

Note the description for each of the OData parameters supported by the Roadmap API. These descriptions are detailed and provide examples of how to use each parameter effectively. They are key to the AI understanding how to construct valid queries when invoking the getM365RoadmapInfo tool.

For example, the $filter parameter description states:

1
2
3
4
5
"FilterParameter": {
        "name": "$filter",
        "in": "query",
        "description": "OData filter expression. IMPORTANT: Use 'created' field for created date filtering (not 'createdDateTime'). Use the 'modified' field for last modified date filtering (not 'lastModifiedDateTime'). Supports contains(), tolower(), and date comparisons. Filter the results based on specific conditions. Use `contains(tolower(title),tolower('search term'))` to search within the title property for a specific term. For creation date filtering, use the 'created' property with ISO 8601 date format. For example, `created ge 2025-03-01T00:00:00Z` to filter roadmap items created after March 1, 2025. To combine filters, use `and` operator. For example, `contains(tolower(title),tolower('Teams')) and created ge 2025-03-01T00:00:00Z`. For the 'status' field, you can filter by status using `status eq 'In development'`, `status eq 'Rolling out'`, `status eq 'Launched'`, `status eq 'Preview'`, `status eq 'Planned'`, or `status eq 'Cancelled'`. For array fields, use the any() operator: For 'platforms' array, use `platforms/any(p: contains(tolower(p), 'desktop'))` to find items available on desktop platform, or `platforms/any(p: p eq 'Web')` for exact platform match. For 'products' array, use `products/any(p: contains(tolower(p), 'teams'))` to search within product names. For 'releaseRings' array, use `releaseRings/any(r: contains(tolower(r), 'general'))` to filter by release ring. For 'cloudInstances' array, use `cloudInstances/any(c: contains(tolower(c), 'gcc'))` to filter by cloud instance. For 'availabilities' array, use nested object filtering: `availabilities/any(a: a/ring eq 'General Availability')` to filter by release ring, `availabilities/any(a: a/year eq 2026)` to filter by year, or `availabilities/any(a: a/month eq 'March')` to filter by month. Date range filtering example: `created ge 2025-01-01T00:00:00Z and created le 2025-12-31T23:59:59Z`. For complex date ranges spanning multiple months and years, ALWAYS use proper parentheses grouping: `(availabilities/any(a: a/year eq 2025 and (a/month eq 'November' or a/month eq 'December')) or availabilities/any(a: a/year eq 2026 and a/month eq 'January'))`. Complex example: `contains(tolower(title), 'teams') and platforms/any(p: p eq 'Desktop') and (availabilities/any(a: a/year eq 2025 and (a/month eq 'November' or a/month eq 'December')) or availabilities/any(a: a/year eq 2026 and a/month eq 'January'))`. To find roadmap items that are 'major changes', use contains(tolower(title), 'major') or contains(tolower(title), 'breaking') or contains(tolower(description), 'major') or contains(tolower(description), 'significant'). Do not use '$filter=isMajorChange+eq+true' as it is not supported by the API."
}

Why use OpenAPI as the source of truth?

OpenAPI is a fair “source of truth” choice because it’s typically more portable, more discoverable, and more ecosystem-friendly than MCP tool schemas today.

That’s not a knock on MCP—it’s because OpenAPI and MCP optimize for different problems:

  • OpenAPI describes HTTP APIs. It answers: “How do I call this service?”
  • MCP describes agent tools. It answers: “How should an agent use this capability?”

In practice, OpenAPI has a few advantages that matter immediately:

  1. It’s a mature, widely adopted contract. OpenAPI (OAS 3.x) is stable and has deep tooling support across gateways, SDK generation, docs (Swagger UI/Redoc), testing, and increasingly AI ingestion.
  2. It’s easy to distribute and version. Specs are commonly published and treated as first-class artifacts in API programs, so they’re easy to find, review, and govern.
  3. It avoids framework lock-in. An OpenAPI spec stands on its own; MCP schemas are usually coupled to MCP lifecycle semantics (initialize, tools/list, tools/call) and server runtime conventions.

The mental model I like:

OpenAPI is the system-of-record contract. MCP schema is the agent-consumable projection.

So the “wins” you get by making OpenAPI authoritative are still the practical ones:

  • Correctness: tool inputs match what the upstream API actually accepts.
  • Maintainability: changes become “update spec → regenerate” instead of hand-syncing schemas.
  • Safety & predictability: strict schemas reject invalid inputs early (especially important when the caller is an LLM).

Even if this server only exposes one tool right now, starting from OpenAPI prevents schema drift later—and lets the MCP layer stay small and purpose-built.

The code toolset (what does what)

It helps to think of this server as three layers:

  • Express (the web wrapper): exposes one HTTP endpoint (/mcp) and passes requests/responses through. (https://www.npmjs.com/package/express)
  • MCP SDK (the protocol engine): speaks MCP/JSON-RPC, advertises available tools, validates tool inputs/outputs, and routes tools/call to the right handler (supports JSON or SSE responses). (https://www.npmjs.com/package/@modelcontextprotocol/sdk)
  • The tool handler: takes the validated arguments, calls the Microsoft 365 Roadmap API with OData query params, and returns a clean result back to the client.

End-to-end request flow

At runtime, a call to getM365RoadmapInfo looks like:

  1. MCP receives a tool invocation with JSON arguments.
  2. Server validates arguments against the generated input Schema.
  3. Server constructs the outbound request to https://www.microsoft.com/releasecommunications/api/v2/m365.
  4. Server normalizes the response into a stable, tool-friendly shape.

Try it yourself

This is the fastest path to a first successful local deployment and a first successful call.

Prequisites:

  • Node.js 20+ installed - https://nodejs.org/
  • Git installed - https://git-scm.com/
  • A terminal (Command Prompt, PowerShell, Bash, etc.)
  • Familiarity with terminal commands
  • Basic understanding of HTTP requests (curl or similar)
  • Familiarity with npm (Node Package Manager)
  1. Clone the repo:
1
2
git clone https://github.com/mjfusa/mcp-roadmap-server
cd mcp-roadmap-server
  1. Install dependencies:
1
npm install
  1. Run the server:
1
npm run dev
  1. Start a new terminal and call the health endpoint:
1
curl -sS -i http://localhost:8081/healthz

You should see a 200 OK response. With the output: {“ok”:true}.

  1. Call the MCP endpoint to fetch 5 roadmap items:
1
2
3
4
curl -sS -X POST 'http://localhost:8081/mcp' `
  -H 'Content-Type: application/json' `
  -H 'Accept: application/json, text/event-stream' `
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"getM365RoadmapInfo","arguments":{"top":5,"orderby":"created desc","count":true}}}'

You should see a valid MCP response with 5 roadmap items.

Congratulations!

You’ve deployed and tested your first MCP server locally! You can now explore the code, understand how the pieces fit together, and prepare for Part 2 where we’ll build a custom agent to call this server.

What’s next

In Part 2 we’ll integrate this M365 Roadmap MCP server into a custom agent built with Copilot Studio, so you can see how MCP tools get discovered and invoked from an agent experience.

Built with Hugo
Theme Stack designed by Jimmy