MCP Server Example on MCP SDK#534
Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the MCP Teams bot example from @examples/mcp to a new @examples/mcp-server workspace and expands it into a more complete MCP server that can message Teams users and support Q&A + approval flows via MCP tools.
Changes:
- Replace the old minimal MCP bot example (
examples/mcp) with a new MCP server implementation (examples/mcp-server) using@modelcontextprotocol/sdk’sMcpServer+ Streamable HTTP transport. - Add in-memory state + Teams handlers to support
notify,ask/getReply, andrequestApproval/getApprovaltools. - Update repo docs/lockfile to reflect the new example package and dependency set.
Reviewed changes
Copilot reviewed 11 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| package-lock.json | Updates workspace entries and adds new deps for the MCP server example (including MCP inspector). |
| examples/mcp/src/index.ts | Removes the previous minimal MCP bot implementation. |
| examples/mcp/appPackage/manifest.json | Removes the old Teams app manifest for the prior example. |
| examples/mcp/appPackage/outline.png | Removes old app package asset (example app icon). |
| examples/mcp/appPackage/color.png | Removes old app package asset (example app icon). |
| examples/mcp/README.md | Removes the old example documentation. |
| examples/mcp-server/turbo.json | Adds Turbo build config for the new example workspace. |
| examples/mcp-server/tsconfig.json | Adds TypeScript config for building the new example. |
| examples/mcp-server/src/state.ts | Adds in-memory state maps for conversation IDs, asks, and approvals. |
| examples/mcp-server/src/mcpTools.ts | Implements MCP tool registrations and proactive Teams messaging/approval cards. |
| examples/mcp-server/src/index.ts | Boots the app and mounts the MCP Streamable HTTP transport at /mcp. |
| examples/mcp-server/src/app.ts | Adds Teams message and card.action handlers to capture replies and approvals. |
| examples/mcp-server/package.json | Renames package and updates deps/devDeps/scripts for the new MCP server example. |
| examples/mcp-server/eslint.config.js | Adds ESLint configuration for the new workspace. |
| examples/mcp-server/README.md | Adds documentation for tools, layout, and run/inspector flow. |
| examples/mcp-server/CHANGELOG.md | Updates changelog header for the renamed example package. |
| examples/mcp-server/.gitignore | Adds workspace-local ignores (env, infra, node_modules, etc.). |
| README.md | Updates the root examples list to point to @examples/mcp-server. |
| .gitignore | Adds .claude/settings.local.json to ignored local files. |
Comments suppressed due to low confidence (1)
examples/mcp-server/package.json:39
@modelcontextprotocol/inspector@^0.21.2requires Node>=22.7.5(per the lockfile), but the repo’s rootpackage.jsondeclaresengines.node >=20. This can cause install-time engine warnings or failures in CI/dev environments pinned to Node 20. Consider pinning the inspector to a version that supports Node 20, or updating the repo/tooling Node requirement consistently.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| adapter.post(MCP_PATH, handle); | ||
| adapter.get(MCP_PATH, handle); | ||
| adapter.delete(MCP_PATH, handle); |
There was a problem hiding this comment.
The /mcp endpoint is mounted with no authentication/authorization, but the exposed tools can proactively message arbitrary userIds and record approvals. Even for an example, leaving this open on the network makes it easy to abuse (spam users, force approval state changes) if the port is reachable. Consider adding a simple guard (e.g., require a bearer token/shared secret in headers for /mcp, or restrict to localhost in dev) and document it in the README.
There was a problem hiding this comment.
I think this is a real gap that at least needs to be addressed in the README as non-production code.
| const handle = async (req: any, res: any) => { | ||
| await transport.handleRequest(req, res, req.body); | ||
| }; |
There was a problem hiding this comment.
handle uses req: any, res: any, which loses type safety and makes it easy to miss issues like missing body parsing. Since this file already depends on Express via ExpressAdapter, consider typing these as express.Request / express.Response (or at least unknown with narrowing) to catch integration mistakes at compile time.
| const transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: () => randomUUID(), | ||
| }); | ||
| await mcpServer.connect(transport); | ||
|
|
||
| const handle = async (req: any, res: any) => { | ||
| await transport.handleRequest(req, res, req.body); | ||
| }; | ||
| adapter.post(MCP_PATH, handle); | ||
| adapter.get(MCP_PATH, handle); | ||
| adapter.delete(MCP_PATH, handle); |
There was a problem hiding this comment.
adapter.post(MCP_PATH, handle) is mounting the MCP endpoint without any JSON body parsing middleware. ExpressAdapter.registerRoute() adds express.json() for the Teams messaging endpoint, but routes added via adapter.post() here won’t get that automatically, so req.body will usually be undefined and transport.handleRequest(req, res, req.body) can fail to parse requests. Add express.json() (or equivalent) for the /mcp routes (e.g., adapter.use(express.json()) before registering, or pass the middleware just for these routes).
| await send('Hi! Will let you know if I need anything.'); | ||
| }); | ||
|
|
||
| app.on('card.action', async ({ activity }) => { |
There was a problem hiding this comment.
teams.ts/examples/cards/src/index.ts
Line 318 in a2dd14b
There was a problem hiding this comment.
app.on('card.action.approval_response', ...)
| return { | ||
| statusCode: 200, | ||
| type: 'application/vnd.microsoft.activity.message', | ||
| value: 'Response recorded', |
There was a problem hiding this comment.
I believe just returning 'Response recorded' should be enough
| // Initialize first so /api/messages is registered before we mount /mcp. | ||
| await app.initialize(); | ||
|
|
||
| const adapter = app.server.adapter; |
There was a problem hiding this comment.
I would recommend managing express outside yourself.
There was a problem hiding this comment.
See the http adapter example on how that works
corinagum
left a comment
There was a problem hiding this comment.
This sample won't start without TENANT_ID in env vars; I think in another sample either in TS or PY we started getting more explicit about tenant id. Here it should be added to the README as a pre-req.
| await app.send(conversationId, question); | ||
| state.pendingAsks.set(requestId, { userId, status: 'pending' }); | ||
| state.userPendingAsk.set(userId, requestId); | ||
| return { requestId }; |
There was a problem hiding this comment.
Should probably note in the README that there's only one ask per user.
| adapter.post(MCP_PATH, handle); | ||
| adapter.get(MCP_PATH, handle); | ||
| adapter.delete(MCP_PATH, handle); |
There was a problem hiding this comment.
I think this is a real gap that at least needs to be addressed in the README as non-production code.
rido-min
left a comment
There was a problem hiding this comment.
still trying to get the sample running
There was a problem hiding this comment.
@rajan-chari this is another proof of why we should have State in the SDK, so samples like these can benefit of that abstraction
There was a problem hiding this comment.
can we use the .gitignore at the root?
| @@ -0,0 +1,164 @@ | |||
| import { randomUUID } from 'crypto'; | |||
|
|
|||
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | |||
There was a problem hiding this comment.
why the .js? I think we can avoid it by customizing the tsconfig
| @@ -16,22 +16,22 @@ | |||
| "build": "npx tsc", | |||
There was a problem hiding this comment.
if tsc is installed from package.json we dont need npx
| @@ -16,22 +16,22 @@ | |||
| "build": "npx tsc", | |||
| "start": "node -r dotenv/config .", | |||
There was a problem hiding this comment.
can we use --env-file instead of dotenv/config ?
| "@modelcontextprotocol/sdk": "^1.25.2", | ||
| "zod": "^3.24.3" | ||
| }, | ||
| "devDependencies": { |
| "inspect": "npx cross-env SERVER_PORT=9000 npx @modelcontextprotocol/inspector -e NODE_NO_WARNINGS=1 -e PORT=3978 node -r dotenv/config .", | ||
| "inspect": "npx cross-env SERVER_PORT=9000 npx @modelcontextprotocol/inspector", | ||
| "dev:teamsfx": "NODE_OPTIONS='--inspect=9239' npx env-cmd -f .env npm run dev", | ||
| "dev:teamsfx:testtool": "NODE_OPTIONS='--inspect=9239' npx env-cmd -f .env npm run dev", |
There was a problem hiding this comment.
I'm missing a note on how to configure this sample
There was a problem hiding this comment.
there are some vulns, we should update the lock
17 vulnerabilities (7 low, 6 moderate, 3 high, 1 critical)
| { | ||
| "name": "@examples/mcp", | ||
| "name": "@examples/mcp-server", | ||
| "version": "0.0.6", |
This pull request migrates and refactors the MCP (Model Context Protocol) Teams bot example from
@examples/mcpto@examples/mcp-server, significantly expanding its functionality and improving its documentation and code structure. The new implementation provides a robust MCP server that exposes human-in-the-loop tools for Teams, including notification, question/answer, and approval flows.Key changes include:
1. Migration and Renaming
@examples/mcpto@examples/mcp-server, with all related files and documentation updated to reflect the new name. (README.md,CHANGELOG.md,package.json, directory and file renames)2. Major Feature Expansion
notify,ask,getReply,requestApproval, andgetApproval. These tools allow agents or clients to interact with real users via Teams for notifications, Q&A, and approval workflows.4. Removal of Old Implementation
README.md,manifest.json, and TypeScript entry point, which are replaced with the new, more capable server.