mcp-teams-reader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +128 -0
- package/dist/graph.d.ts +57 -0
- package/dist/graph.js +78 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +236 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# teams-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP Server for Microsoft Teams — search, read and summarize messages via Claude / AI assistants.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- List joined Teams and channels
|
|
8
|
+
- Read channel messages (with time range filter)
|
|
9
|
+
- List and read 1:1 / group chats
|
|
10
|
+
- Search messages by keyword across chats and channels
|
|
11
|
+
- Deep links to open messages in Teams
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx teams-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
### 1. Register an Azure AD App
|
|
22
|
+
|
|
23
|
+
1. Go to [Azure Portal](https://portal.azure.com) → **App registrations** → **New registration**
|
|
24
|
+
2. Set **Supported account types** to your org (single tenant)
|
|
25
|
+
3. Under **Authentication**, enable **Allow public client flows** (for Device Code Flow)
|
|
26
|
+
4. Under **API permissions**, add these **Delegated** permissions for Microsoft Graph:
|
|
27
|
+
- `Chat.Read`
|
|
28
|
+
- `ChannelMessage.Read.All`
|
|
29
|
+
- `Team.ReadBasic.All`
|
|
30
|
+
- `Channel.ReadBasic.All`
|
|
31
|
+
- `User.Read`
|
|
32
|
+
5. Click **Grant admin consent** (or ask your admin)
|
|
33
|
+
|
|
34
|
+
### 2. Configure MCP Client
|
|
35
|
+
|
|
36
|
+
Add to your MCP client config (e.g. Claude Code `settings.json` or `claude_desktop_config.json`):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"teams": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["teams-mcp-server"],
|
|
44
|
+
"env": {
|
|
45
|
+
"TEAMS_CLIENT_ID": "your-azure-app-client-id",
|
|
46
|
+
"TEAMS_TENANT_ID": "your-azure-tenant-id"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Authenticate
|
|
54
|
+
|
|
55
|
+
On first use, any tool call will return a Device Code login prompt. Open the URL in your browser, enter the code, and sign in. The token is cached locally for future use.
|
|
56
|
+
|
|
57
|
+
## Available Tools
|
|
58
|
+
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `list_my_teams` | List all Teams you've joined |
|
|
62
|
+
| `list_channels` | List channels in a team |
|
|
63
|
+
| `get_channel_messages` | Get channel messages (with optional time filter) |
|
|
64
|
+
| `list_my_chats` | List recent 1:1 and group chats |
|
|
65
|
+
| `get_chat_messages` | Get messages from a chat |
|
|
66
|
+
| `search_messages` | Search messages by keyword across chats |
|
|
67
|
+
| `search_channel_messages` | Search messages by keyword in a channel |
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone <repo-url>
|
|
73
|
+
cd teams-mcp-server
|
|
74
|
+
npm install
|
|
75
|
+
cp .env.example .env # fill in your Client ID and Tenant ID
|
|
76
|
+
npm run dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when device code authentication is needed.
|
|
3
|
+
* The message contains the device code instructions for the user.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DeviceCodeRequiredError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get an access token, using cached token if available.
|
|
10
|
+
* If device code auth is needed, throws DeviceCodeRequiredError
|
|
11
|
+
* with the auth instructions, and starts the flow in the background.
|
|
12
|
+
* On next call, if auth completed, returns the token.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAccessToken(): Promise<string>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { PublicClientApplication, } from "@azure/msal-node";
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
const TOKEN_CACHE_PATH = path.join(import.meta.dirname ?? ".", "..", "token-cache.json");
|
|
6
|
+
const SCOPES = [
|
|
7
|
+
"Chat.Read",
|
|
8
|
+
"ChannelMessage.Read.All",
|
|
9
|
+
"Team.ReadBasic.All",
|
|
10
|
+
"Channel.ReadBasic.All",
|
|
11
|
+
"User.Read",
|
|
12
|
+
];
|
|
13
|
+
let msalClient = null;
|
|
14
|
+
// Background auth state
|
|
15
|
+
let pendingAuthPromise = null;
|
|
16
|
+
let pendingDeviceCodeMessage = null;
|
|
17
|
+
function getConfig() {
|
|
18
|
+
const clientId = process.env.TEAMS_CLIENT_ID;
|
|
19
|
+
const tenantId = process.env.TEAMS_TENANT_ID;
|
|
20
|
+
if (!clientId || !tenantId) {
|
|
21
|
+
throw new Error("Missing TEAMS_CLIENT_ID or TEAMS_TENANT_ID environment variables");
|
|
22
|
+
}
|
|
23
|
+
return { clientId, tenantId };
|
|
24
|
+
}
|
|
25
|
+
async function getMsalClient() {
|
|
26
|
+
if (msalClient)
|
|
27
|
+
return msalClient;
|
|
28
|
+
const { clientId, tenantId } = getConfig();
|
|
29
|
+
msalClient = new PublicClientApplication({
|
|
30
|
+
auth: {
|
|
31
|
+
clientId,
|
|
32
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
33
|
+
},
|
|
34
|
+
cache: {
|
|
35
|
+
cachePlugin: {
|
|
36
|
+
beforeCacheAccess: async (ctx) => {
|
|
37
|
+
if (existsSync(TOKEN_CACHE_PATH)) {
|
|
38
|
+
const data = await readFile(TOKEN_CACHE_PATH, "utf-8");
|
|
39
|
+
ctx.tokenCache.deserialize(data);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
afterCacheAccess: async (ctx) => {
|
|
43
|
+
if (ctx.cacheHasChanged) {
|
|
44
|
+
await writeFile(TOKEN_CACHE_PATH, ctx.tokenCache.serialize(), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return msalClient;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Error thrown when device code authentication is needed.
|
|
54
|
+
* The message contains the device code instructions for the user.
|
|
55
|
+
*/
|
|
56
|
+
export class DeviceCodeRequiredError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "DeviceCodeRequiredError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get an access token, using cached token if available.
|
|
64
|
+
* If device code auth is needed, throws DeviceCodeRequiredError
|
|
65
|
+
* with the auth instructions, and starts the flow in the background.
|
|
66
|
+
* On next call, if auth completed, returns the token.
|
|
67
|
+
*/
|
|
68
|
+
export async function getAccessToken() {
|
|
69
|
+
const client = await getMsalClient();
|
|
70
|
+
// Try silent acquisition first (cached token)
|
|
71
|
+
const accounts = await client.getTokenCache().getAllAccounts();
|
|
72
|
+
if (accounts.length > 0) {
|
|
73
|
+
try {
|
|
74
|
+
const result = await client.acquireTokenSilent({
|
|
75
|
+
account: accounts[0],
|
|
76
|
+
scopes: SCOPES,
|
|
77
|
+
});
|
|
78
|
+
if (result?.accessToken)
|
|
79
|
+
return result.accessToken;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Silent acquisition failed, fall through to device code
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// If there's a pending auth, check if it completed
|
|
86
|
+
if (pendingAuthPromise) {
|
|
87
|
+
try {
|
|
88
|
+
const token = await Promise.race([
|
|
89
|
+
pendingAuthPromise,
|
|
90
|
+
// Give it 2 seconds to check if already done
|
|
91
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("still_pending")), 2000)),
|
|
92
|
+
]);
|
|
93
|
+
pendingAuthPromise = null;
|
|
94
|
+
pendingDeviceCodeMessage = null;
|
|
95
|
+
return token;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err?.message === "still_pending") {
|
|
99
|
+
// Still waiting for user to authenticate
|
|
100
|
+
throw new DeviceCodeRequiredError(`⏳ 认证进行中,请先在浏览器中完成登录,然后再试一次。\n\n${pendingDeviceCodeMessage ?? ""}`);
|
|
101
|
+
}
|
|
102
|
+
// Auth failed, reset and try again
|
|
103
|
+
pendingAuthPromise = null;
|
|
104
|
+
pendingDeviceCodeMessage = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Start device code flow in background
|
|
108
|
+
const deviceCodeRequest = {
|
|
109
|
+
scopes: SCOPES,
|
|
110
|
+
deviceCodeCallback: (response) => {
|
|
111
|
+
pendingDeviceCodeMessage = response.message;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
pendingAuthPromise = client
|
|
115
|
+
.acquireTokenByDeviceCode(deviceCodeRequest)
|
|
116
|
+
.then((result) => {
|
|
117
|
+
if (!result?.accessToken) {
|
|
118
|
+
throw new Error("Failed to acquire access token");
|
|
119
|
+
}
|
|
120
|
+
return result.accessToken;
|
|
121
|
+
});
|
|
122
|
+
// Wait briefly for the device code callback to fire
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
124
|
+
const message = pendingDeviceCodeMessage
|
|
125
|
+
? `🔐 需要登录 Microsoft 账号:\n\n${pendingDeviceCodeMessage}\n\n完成登录后,请再次调用此工具。`
|
|
126
|
+
: "🔐 正在获取认证信息,请稍后再试...";
|
|
127
|
+
throw new DeviceCodeRequiredError(message);
|
|
128
|
+
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
interface Team {
|
|
2
|
+
id: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
interface Channel {
|
|
7
|
+
id: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
webUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ChatMessage {
|
|
13
|
+
id: string;
|
|
14
|
+
createdDateTime: string;
|
|
15
|
+
body: {
|
|
16
|
+
content: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
};
|
|
19
|
+
from?: {
|
|
20
|
+
user?: {
|
|
21
|
+
displayName: string;
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
webUrl?: string;
|
|
26
|
+
channelIdentity?: {
|
|
27
|
+
teamId: string;
|
|
28
|
+
channelId: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface Chat {
|
|
32
|
+
id: string;
|
|
33
|
+
topic?: string;
|
|
34
|
+
chatType: string;
|
|
35
|
+
lastUpdatedDateTime?: string;
|
|
36
|
+
members?: Array<{
|
|
37
|
+
displayName?: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
/** List all teams the user is a member of */
|
|
41
|
+
export declare function listMyTeams(): Promise<Team[]>;
|
|
42
|
+
/** List channels in a team */
|
|
43
|
+
export declare function listChannels(teamId: string): Promise<Channel[]>;
|
|
44
|
+
/** Get messages from a team channel, optionally filtered by date */
|
|
45
|
+
export declare function getChannelMessages(teamId: string, channelId: string, since?: string, top?: number): Promise<ChatMessage[]>;
|
|
46
|
+
/** List the user's 1:1 and group chats */
|
|
47
|
+
export declare function listMyChats(top?: number): Promise<Chat[]>;
|
|
48
|
+
/** Get messages from a 1:1 or group chat */
|
|
49
|
+
export declare function getChatMessages(chatId: string, since?: string, top?: number): Promise<ChatMessage[]>;
|
|
50
|
+
/** Search messages across chats using keyword */
|
|
51
|
+
export declare function searchMessages(keyword: string, daysBack?: number): Promise<Array<ChatMessage & {
|
|
52
|
+
chatId: string;
|
|
53
|
+
chatTopic?: string;
|
|
54
|
+
}>>;
|
|
55
|
+
/** Build a deep link to a Teams message */
|
|
56
|
+
export declare function buildTeamsLink(teamId: string, channelId: string, messageId: string): string;
|
|
57
|
+
export {};
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getAccessToken } from "./auth.js";
|
|
2
|
+
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
3
|
+
async function graphFetch(endpoint) {
|
|
4
|
+
const token = await getAccessToken();
|
|
5
|
+
const res = await fetch(`${GRAPH_BASE}${endpoint}`, {
|
|
6
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
7
|
+
});
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
const errText = await res.text();
|
|
10
|
+
throw new Error(`Graph API error ${res.status}: ${errText}`);
|
|
11
|
+
}
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
/** List all teams the user is a member of */
|
|
15
|
+
export async function listMyTeams() {
|
|
16
|
+
const data = await graphFetch("/me/joinedTeams");
|
|
17
|
+
return data.value;
|
|
18
|
+
}
|
|
19
|
+
/** List channels in a team */
|
|
20
|
+
export async function listChannels(teamId) {
|
|
21
|
+
const data = await graphFetch(`/teams/${teamId}/channels`);
|
|
22
|
+
return data.value;
|
|
23
|
+
}
|
|
24
|
+
/** Get messages from a team channel, optionally filtered by date */
|
|
25
|
+
export async function getChannelMessages(teamId, channelId, since, top = 50) {
|
|
26
|
+
let endpoint = `/teams/${teamId}/channels/${channelId}/messages?$top=${top}`;
|
|
27
|
+
const data = await graphFetch(endpoint);
|
|
28
|
+
let messages = data.value;
|
|
29
|
+
// Client-side filter by date if `since` is provided
|
|
30
|
+
if (since) {
|
|
31
|
+
const sinceDate = new Date(since);
|
|
32
|
+
messages = messages.filter((m) => new Date(m.createdDateTime) >= sinceDate);
|
|
33
|
+
}
|
|
34
|
+
return messages;
|
|
35
|
+
}
|
|
36
|
+
/** List the user's 1:1 and group chats */
|
|
37
|
+
export async function listMyChats(top = 50) {
|
|
38
|
+
const data = await graphFetch(`/me/chats?$top=${top}`);
|
|
39
|
+
return data.value;
|
|
40
|
+
}
|
|
41
|
+
/** Get messages from a 1:1 or group chat */
|
|
42
|
+
export async function getChatMessages(chatId, since, top = 50) {
|
|
43
|
+
const data = await graphFetch(`/me/chats/${chatId}/messages?$top=${top}`);
|
|
44
|
+
let messages = data.value;
|
|
45
|
+
if (since) {
|
|
46
|
+
const sinceDate = new Date(since);
|
|
47
|
+
messages = messages.filter((m) => new Date(m.createdDateTime) >= sinceDate);
|
|
48
|
+
}
|
|
49
|
+
return messages;
|
|
50
|
+
}
|
|
51
|
+
/** Search messages across chats using keyword */
|
|
52
|
+
export async function searchMessages(keyword, daysBack = 7) {
|
|
53
|
+
const chats = await listMyChats(50);
|
|
54
|
+
const since = new Date();
|
|
55
|
+
since.setDate(since.getDate() - daysBack);
|
|
56
|
+
const sinceISO = since.toISOString();
|
|
57
|
+
const results = [];
|
|
58
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
59
|
+
// Search through recent chats
|
|
60
|
+
for (const chat of chats.slice(0, 20)) {
|
|
61
|
+
try {
|
|
62
|
+
const messages = await getChatMessages(chat.id, sinceISO, 50);
|
|
63
|
+
for (const msg of messages) {
|
|
64
|
+
if (msg.body.content.toLowerCase().includes(lowerKeyword)) {
|
|
65
|
+
results.push({ ...msg, chatId: chat.id, chatTopic: chat.topic ?? undefined });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Skip chats that error out
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
/** Build a deep link to a Teams message */
|
|
76
|
+
export function buildTeamsLink(teamId, channelId, messageId) {
|
|
77
|
+
return `https://teams.microsoft.com/l/message/${channelId}/${messageId}?tenantId=&groupId=${teamId}&parentMessageId=${messageId}`;
|
|
78
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { listMyTeams, listChannels, getChannelMessages, listMyChats, getChatMessages, searchMessages, buildTeamsLink, } from "./graph.js";
|
|
6
|
+
import { DeviceCodeRequiredError } from "./auth.js";
|
|
7
|
+
function handleError(err) {
|
|
8
|
+
if (err instanceof DeviceCodeRequiredError) {
|
|
9
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
10
|
+
}
|
|
11
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12
|
+
return { content: [{ type: "text", text: `错误: ${msg}` }], isError: true };
|
|
13
|
+
}
|
|
14
|
+
function stripHtml(html) {
|
|
15
|
+
return html
|
|
16
|
+
.replace(/<[^>]*>/g, "")
|
|
17
|
+
.replace(/ /g, " ")
|
|
18
|
+
.replace(/&/g, "&")
|
|
19
|
+
.replace(/</g, "<")
|
|
20
|
+
.replace(/>/g, ">")
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
function formatDate(iso) {
|
|
25
|
+
return new Date(iso).toLocaleString("zh-CN", { timeZone: "Asia/Tokyo" });
|
|
26
|
+
}
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: "teams-mcp-server",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
});
|
|
31
|
+
// ============ Tool: list_my_teams ============
|
|
32
|
+
server.tool("list_my_teams", "列出我加入的所有 Teams 团队", {}, async () => {
|
|
33
|
+
try {
|
|
34
|
+
const teams = await listMyTeams();
|
|
35
|
+
const text = teams
|
|
36
|
+
.map((t) => `- **${t.displayName}** (ID: ${t.id})${t.description ? ` — ${t.description}` : ""}`)
|
|
37
|
+
.join("\n");
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: text || "没有找到任何团队。" }],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return handleError(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// ============ Tool: list_channels ============
|
|
47
|
+
server.tool("list_channels", "列出某个团队下的所有频道", { team_id: z.string().describe("团队 ID,可通过 list_my_teams 获取") }, async ({ team_id }) => {
|
|
48
|
+
try {
|
|
49
|
+
const channels = await listChannels(team_id);
|
|
50
|
+
const text = channels
|
|
51
|
+
.map((c) => `- **${c.displayName}** (ID: ${c.id})${c.description ? ` — ${c.description}` : ""}`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: text || "该团队没有频道。" }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return handleError(err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// ============ Tool: get_channel_messages ============
|
|
62
|
+
server.tool("get_channel_messages", "获取团队频道的消息,可指定时间范围。用于总结频道讨论内容。", {
|
|
63
|
+
team_id: z.string().describe("团队 ID"),
|
|
64
|
+
channel_id: z.string().describe("频道 ID"),
|
|
65
|
+
since: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("起始时间 ISO 格式,如 2026-03-06T00:00:00Z,不填则获取最近消息"),
|
|
69
|
+
limit: z.number().optional().default(50).describe("最大消息数,默认 50"),
|
|
70
|
+
}, async ({ team_id, channel_id, since, limit }) => {
|
|
71
|
+
try {
|
|
72
|
+
const messages = await getChannelMessages(team_id, channel_id, since, limit);
|
|
73
|
+
if (messages.length === 0) {
|
|
74
|
+
return { content: [{ type: "text", text: "该时间范围内没有消息。" }] };
|
|
75
|
+
}
|
|
76
|
+
const text = messages
|
|
77
|
+
.map((m) => {
|
|
78
|
+
const sender = m.from?.user?.displayName ?? "Unknown";
|
|
79
|
+
const time = formatDate(m.createdDateTime);
|
|
80
|
+
const body = stripHtml(m.body.content);
|
|
81
|
+
const link = buildTeamsLink(team_id, channel_id, m.id);
|
|
82
|
+
return `**${sender}** (${time}):\n${body}\n[打开消息](${link})`;
|
|
83
|
+
})
|
|
84
|
+
.join("\n\n---\n\n");
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `找到 ${messages.length} 条消息:\n\n${text}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return handleError(err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// ============ Tool: list_my_chats ============
|
|
99
|
+
server.tool("list_my_chats", "列出我最近的私聊和群聊", {
|
|
100
|
+
limit: z.number().optional().default(20).describe("返回数量,默认 20"),
|
|
101
|
+
}, async ({ limit }) => {
|
|
102
|
+
try {
|
|
103
|
+
const chats = await listMyChats(limit);
|
|
104
|
+
const text = chats
|
|
105
|
+
.map((c) => {
|
|
106
|
+
const topic = c.topic || `(${c.chatType} chat)`;
|
|
107
|
+
const updated = c.lastUpdatedDateTime
|
|
108
|
+
? formatDate(c.lastUpdatedDateTime)
|
|
109
|
+
: "";
|
|
110
|
+
return `- **${topic}** (ID: ${c.id}) — 最后更新: ${updated}`;
|
|
111
|
+
})
|
|
112
|
+
.join("\n");
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: text || "没有找到聊天。" }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return handleError(err);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// ============ Tool: get_chat_messages ============
|
|
122
|
+
server.tool("get_chat_messages", "获取某个私聊/群聊的消息", {
|
|
123
|
+
chat_id: z.string().describe("聊天 ID,可通过 list_my_chats 获取"),
|
|
124
|
+
since: z.string().optional().describe("起始时间 ISO 格式"),
|
|
125
|
+
limit: z.number().optional().default(50).describe("最大消息数"),
|
|
126
|
+
}, async ({ chat_id, since, limit }) => {
|
|
127
|
+
try {
|
|
128
|
+
const messages = await getChatMessages(chat_id, since, limit);
|
|
129
|
+
if (messages.length === 0) {
|
|
130
|
+
return { content: [{ type: "text", text: "没有消息。" }] };
|
|
131
|
+
}
|
|
132
|
+
const text = messages
|
|
133
|
+
.map((m) => {
|
|
134
|
+
const sender = m.from?.user?.displayName ?? "Unknown";
|
|
135
|
+
const time = formatDate(m.createdDateTime);
|
|
136
|
+
const body = stripHtml(m.body.content);
|
|
137
|
+
return `**${sender}** (${time}):\n${body}`;
|
|
138
|
+
})
|
|
139
|
+
.join("\n\n---\n\n");
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: `${messages.length} 条消息:\n\n${text}` }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return handleError(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// ============ Tool: search_messages ============
|
|
149
|
+
server.tool("search_messages", "按关键词搜索最近的聊天消息(搜索私聊和群聊)", {
|
|
150
|
+
keyword: z.string().describe("搜索关键词"),
|
|
151
|
+
days_back: z.number().optional().default(7).describe("搜索最近几天,默认 7 天"),
|
|
152
|
+
}, async ({ keyword, days_back }) => {
|
|
153
|
+
try {
|
|
154
|
+
const results = await searchMessages(keyword, days_back);
|
|
155
|
+
if (results.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{ type: "text", text: `没有找到包含"${keyword}"的消息。` },
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const text = results
|
|
163
|
+
.map((m) => {
|
|
164
|
+
const sender = m.from?.user?.displayName ?? "Unknown";
|
|
165
|
+
const time = formatDate(m.createdDateTime);
|
|
166
|
+
const body = stripHtml(m.body.content);
|
|
167
|
+
const chatInfo = m.chatTopic ? `[${m.chatTopic}]` : `[Chat: ${m.chatId.slice(0, 8)}...]`;
|
|
168
|
+
const link = m.webUrl
|
|
169
|
+
? m.webUrl
|
|
170
|
+
: `https://teams.microsoft.com/l/message/${m.chatId}/${m.id}?context=${encodeURIComponent(JSON.stringify({ contextType: "chat" }))}`;
|
|
171
|
+
return `${chatInfo} **${sender}** (${time}):\n${body}\n🔗 ${link}`;
|
|
172
|
+
})
|
|
173
|
+
.join("\n\n---\n\n");
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `找到 ${results.length} 条包含"${keyword}"的消息:\n\n${text}`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return handleError(err);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// ============ Tool: search_channel_messages ============
|
|
188
|
+
server.tool("search_channel_messages", "在指定团队频道中按关键词搜索消息", {
|
|
189
|
+
team_id: z.string().describe("团队 ID"),
|
|
190
|
+
channel_id: z.string().describe("频道 ID"),
|
|
191
|
+
keyword: z.string().describe("搜索关键词"),
|
|
192
|
+
since: z.string().optional().describe("起始时间 ISO 格式"),
|
|
193
|
+
}, async ({ team_id, channel_id, keyword, since }) => {
|
|
194
|
+
try {
|
|
195
|
+
const messages = await getChannelMessages(team_id, channel_id, since, 50);
|
|
196
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
197
|
+
const matched = messages.filter((m) => stripHtml(m.body.content).toLowerCase().includes(lowerKeyword));
|
|
198
|
+
if (matched.length === 0) {
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{ type: "text", text: `频道中没有找到包含"${keyword}"的消息。` },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const text = matched
|
|
206
|
+
.map((m) => {
|
|
207
|
+
const sender = m.from?.user?.displayName ?? "Unknown";
|
|
208
|
+
const time = formatDate(m.createdDateTime);
|
|
209
|
+
const body = stripHtml(m.body.content);
|
|
210
|
+
const link = buildTeamsLink(team_id, channel_id, m.id);
|
|
211
|
+
return `**${sender}** (${time}):\n${body}\n[打开消息](${link})`;
|
|
212
|
+
})
|
|
213
|
+
.join("\n\n---\n\n");
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `找到 ${matched.length} 条匹配消息:\n\n${text}`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
return handleError(err);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// ============ Start Server ============
|
|
228
|
+
async function main() {
|
|
229
|
+
const transport = new StdioServerTransport();
|
|
230
|
+
await server.connect(transport);
|
|
231
|
+
console.error("Teams MCP Server is running");
|
|
232
|
+
}
|
|
233
|
+
main().catch((err) => {
|
|
234
|
+
console.error("Fatal error:", err);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-teams-reader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for Microsoft Teams - search, read and summarize messages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-teams-reader": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"microsoft-teams",
|
|
22
|
+
"teams",
|
|
23
|
+
"claude",
|
|
24
|
+
"ai",
|
|
25
|
+
"model-context-protocol"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": ""
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@azure/msal-node": "^2.16.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsx": "^4.19.0",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
}
|
|
41
|
+
}
|