@teamsly/mcp 0.1.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 +160 -0
- package/dist/index.js +292 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Teamsly MCP Server
|
|
2
|
+
|
|
3
|
+
Exposes your Microsoft Teams as MCP tools — send DMs, read messages, list chats, channels, and teams.
|
|
4
|
+
|
|
5
|
+
Works with any MCP-compatible client: Claude Code, Claude Desktop, Cursor, Zed, Windsurf, and others.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add teamsly -- npx -y @teamsly/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it — no clone required. On first use you'll do a one-time Microsoft sign-in (see **Auth** below). For other clients, use the config blocks further down.
|
|
14
|
+
|
|
15
|
+
## Auth (one-time setup)
|
|
16
|
+
|
|
17
|
+
On first run, a sign-in prompt appears in your terminal:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
╔══════════════════════════════════════════╗
|
|
21
|
+
║ Sign in to Teamsly MCP ║
|
|
22
|
+
╠══════════════════════════════════════════╣
|
|
23
|
+
║ 1. Open: https://microsoft.com/devicelogin
|
|
24
|
+
║ 2. Enter code: XXXXXXXX ║
|
|
25
|
+
╚══════════════════════════════════════════╝
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Open the URL, enter the code, sign in with your Microsoft account. Done — tokens are saved to `~/.config/teamsly-mcp/tokens.json` and auto-refreshed.
|
|
29
|
+
|
|
30
|
+
> **Azure app requirement:** the app registration behind `TEAMSLY_CLIENT_ID` must have **"Allow public client flows"** enabled (Entra ID → App registrations → *app* → Authentication → Advanced settings). The device-code flow is a public-client flow; without this, sign-in fails at the token step with `AADSTS7000218: ... must contain 'client_assertion' or 'client_secret'`.
|
|
31
|
+
|
|
32
|
+
## Tools
|
|
33
|
+
|
|
34
|
+
| Tool | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `find_people` | Search contacts by name → returns `[{ id, displayName, email }]` |
|
|
37
|
+
| `send_dm` | Send a DM given a user ID (use `find_people` first) |
|
|
38
|
+
| `list_chats` | List recent DM and group chats |
|
|
39
|
+
| `get_chat_messages` | Get recent messages from a chat |
|
|
40
|
+
| `send_chat_message` | Send a message to a chat by ID |
|
|
41
|
+
| `list_teams` | List your Teams |
|
|
42
|
+
| `list_channels` | List channels in a team |
|
|
43
|
+
| `get_channel_messages` | Get recent messages from a channel |
|
|
44
|
+
| `send_channel_message` | Post a message to a channel |
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Claude Code
|
|
51
|
+
|
|
52
|
+
**Project-level** (auto-discovered for anyone who opens this repo — already configured via `.mcp.json`):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"teamsly": {
|
|
58
|
+
"command": "npx",
|
|
59
|
+
"args": ["-y", "@teamsly/mcp"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**User-level** (available in all your projects):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude mcp add teamsly -- npx -y @teamsly/mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### Claude Desktop
|
|
74
|
+
|
|
75
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"teamsly": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": ["-y", "@teamsly/mcp"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart Claude Desktop after saving.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### Cursor
|
|
93
|
+
|
|
94
|
+
Edit `~/.cursor/mcp.json` (user-level) or `.cursor/mcp.json` in your project (project-level):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"teamsly": {
|
|
100
|
+
"command": "npx",
|
|
101
|
+
"args": ["-y", "@teamsly/mcp"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Zed
|
|
110
|
+
|
|
111
|
+
Edit `~/.config/zed/settings.json`:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"context_servers": {
|
|
116
|
+
"teamsly": {
|
|
117
|
+
"command": { "path": "npx", "args": ["-y", "@teamsly/mcp"] }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Windsurf
|
|
126
|
+
|
|
127
|
+
Edit `~/.codeium/windsurf/mcp_config.json`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"teamsly": {
|
|
133
|
+
"command": "npx",
|
|
134
|
+
"args": ["-y", "@teamsly/mcp"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Develop from source
|
|
143
|
+
|
|
144
|
+
Working on the server itself? Run it straight from the repo with no build:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx tsx mcp-server/index.ts
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The repo's `.mcp.json` already points Claude Code at this for local development.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Optional env vars
|
|
155
|
+
|
|
156
|
+
| Variable | Default | Description |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `TEAMSLY_CLIENT_ID` | teamsly.app's app ID | Azure AD app client ID |
|
|
159
|
+
| `TEAMSLY_TENANT_ID` | `common` | Tenant ID (use your org's ID to restrict to one tenant) |
|
|
160
|
+
| `TEAMSLY_TOKEN_DIR` | `~/.config/teamsly-mcp` | Token storage directory |
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var CLIENT_ID = process.env.TEAMSLY_CLIENT_ID ?? "377aa8a2-24d1-4d6e-8eca-e347864c9880";
|
|
11
|
+
var TENANT_ID = process.env.TEAMSLY_TENANT_ID ?? "common";
|
|
12
|
+
var TOKEN_DIR = process.env.TEAMSLY_TOKEN_DIR ?? join(homedir(), ".config", "teamsly-mcp");
|
|
13
|
+
var TOKEN_FILE = join(TOKEN_DIR, "tokens.json");
|
|
14
|
+
var SCOPE = [
|
|
15
|
+
"User.Read",
|
|
16
|
+
"User.ReadBasic.All",
|
|
17
|
+
"People.Read",
|
|
18
|
+
"Team.ReadBasic.All",
|
|
19
|
+
"Channel.ReadBasic.All",
|
|
20
|
+
"ChannelMessage.Read.All",
|
|
21
|
+
"ChannelMessage.Send",
|
|
22
|
+
"Chat.ReadWrite",
|
|
23
|
+
"Presence.Read.All",
|
|
24
|
+
"Files.Read.All",
|
|
25
|
+
"Calendars.Read",
|
|
26
|
+
"offline_access"
|
|
27
|
+
].join(" ");
|
|
28
|
+
var GRAPH = "https://graph.microsoft.com/v1.0";
|
|
29
|
+
var TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;
|
|
30
|
+
function loadTokens() {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(TOKEN_FILE, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function saveTokens(t) {
|
|
38
|
+
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
39
|
+
writeFileSync(TOKEN_FILE, JSON.stringify(t, null, 2), { mode: 384 });
|
|
40
|
+
}
|
|
41
|
+
async function deviceCodeAuth() {
|
|
42
|
+
const codeRes = await fetch(
|
|
43
|
+
`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/devicecode`,
|
|
44
|
+
{
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
47
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE })
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
if (!codeRes.ok) throw new Error(`Device code request failed: ${await codeRes.text()}`);
|
|
51
|
+
const { device_code, user_code, verification_uri, interval, expires_in } = await codeRes.json();
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`
|
|
54
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
55
|
+
\u2551 Sign in to Teamsly MCP \u2551
|
|
56
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
57
|
+
\u2551 1. Open: ${verification_uri.padEnd(31)}\u2551
|
|
58
|
+
\u2551 2. Enter code: ${user_code.padEnd(25)}\u2551
|
|
59
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
60
|
+
|
|
61
|
+
Waiting for sign-in\u2026
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
const deadline = Date.now() + expires_in * 1e3;
|
|
65
|
+
const poll = Math.max(interval, 5) * 1e3;
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
await new Promise((r) => setTimeout(r, poll));
|
|
68
|
+
const res = await fetch(TOKEN_URL, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
71
|
+
body: new URLSearchParams({
|
|
72
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
73
|
+
client_id: CLIENT_ID,
|
|
74
|
+
device_code
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
if (data.access_token) {
|
|
79
|
+
const tokens = {
|
|
80
|
+
access_token: data.access_token,
|
|
81
|
+
refresh_token: data.refresh_token ?? "",
|
|
82
|
+
expires_at: Date.now() + (data.expires_in ?? 3600) * 1e3
|
|
83
|
+
};
|
|
84
|
+
saveTokens(tokens);
|
|
85
|
+
process.stderr.write("\u2713 Signed in. Tokens saved.\n\n");
|
|
86
|
+
return tokens;
|
|
87
|
+
}
|
|
88
|
+
if (data.error && data.error !== "authorization_pending" && data.error !== "slow_down") {
|
|
89
|
+
throw new Error(`Auth error: ${data.error}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error("Device code expired \u2014 please restart and try again.");
|
|
93
|
+
}
|
|
94
|
+
async function refreshTokens(stored) {
|
|
95
|
+
const res = await fetch(TOKEN_URL, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
98
|
+
body: new URLSearchParams({
|
|
99
|
+
grant_type: "refresh_token",
|
|
100
|
+
client_id: CLIENT_ID,
|
|
101
|
+
refresh_token: stored.refresh_token,
|
|
102
|
+
scope: SCOPE
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
if (!data.access_token) throw new Error(`Refresh failed: ${data.error}`);
|
|
107
|
+
const tokens = {
|
|
108
|
+
access_token: data.access_token,
|
|
109
|
+
refresh_token: data.refresh_token ?? stored.refresh_token,
|
|
110
|
+
expires_at: Date.now() + (data.expires_in ?? 3600) * 1e3
|
|
111
|
+
};
|
|
112
|
+
saveTokens(tokens);
|
|
113
|
+
return tokens;
|
|
114
|
+
}
|
|
115
|
+
var _tokens = null;
|
|
116
|
+
async function getAccessToken() {
|
|
117
|
+
if (!_tokens) {
|
|
118
|
+
_tokens = loadTokens();
|
|
119
|
+
}
|
|
120
|
+
if (!_tokens) {
|
|
121
|
+
_tokens = await deviceCodeAuth();
|
|
122
|
+
} else if (_tokens.expires_at < Date.now() + 6e4) {
|
|
123
|
+
try {
|
|
124
|
+
_tokens = await refreshTokens(_tokens);
|
|
125
|
+
} catch {
|
|
126
|
+
_tokens = await deviceCodeAuth();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return _tokens.access_token;
|
|
130
|
+
}
|
|
131
|
+
var _myId = null;
|
|
132
|
+
async function getMyId() {
|
|
133
|
+
if (!_myId) {
|
|
134
|
+
const me = await graph("/me?$select=id");
|
|
135
|
+
_myId = me.id;
|
|
136
|
+
}
|
|
137
|
+
return _myId;
|
|
138
|
+
}
|
|
139
|
+
async function graph(path, options = {}) {
|
|
140
|
+
const token = await getAccessToken();
|
|
141
|
+
const res = await fetch(`${GRAPH}${path}`, {
|
|
142
|
+
...options,
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${token}`,
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
...options.headers ?? {}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
const text = await res.text().catch(() => "");
|
|
151
|
+
throw new Error(`Graph ${path} \u2192 ${res.status}: ${text}`);
|
|
152
|
+
}
|
|
153
|
+
if (res.status === 204) return null;
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
156
|
+
var server = new McpServer({ name: "teamsly", version: "2.0.0" });
|
|
157
|
+
server.tool(
|
|
158
|
+
"list_chats",
|
|
159
|
+
"List recent Microsoft Teams DM and group chat conversations",
|
|
160
|
+
{},
|
|
161
|
+
async () => {
|
|
162
|
+
const data = await graph("/me/chats?$expand=members&$top=50");
|
|
163
|
+
return { content: [{ type: "text", text: JSON.stringify(data?.value ?? data, null, 2) }] };
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
server.tool(
|
|
167
|
+
"get_chat_messages",
|
|
168
|
+
"Get recent messages from a Teams DM or group chat",
|
|
169
|
+
{ chat_id: z.string().describe("The chat ID from list_chats") },
|
|
170
|
+
async ({ chat_id }) => {
|
|
171
|
+
const data = await graph(`/me/chats/${encodeURIComponent(chat_id)}/messages?$top=20`);
|
|
172
|
+
return { content: [{ type: "text", text: JSON.stringify(data?.value ?? data, null, 2) }] };
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
server.tool(
|
|
176
|
+
"send_chat_message",
|
|
177
|
+
"Send a message to a Teams DM or group chat",
|
|
178
|
+
{
|
|
179
|
+
chat_id: z.string().describe("The chat ID from list_chats"),
|
|
180
|
+
message: z.string().describe("Plain text message to send")
|
|
181
|
+
},
|
|
182
|
+
async ({ chat_id, message }) => {
|
|
183
|
+
await graph(`/me/chats/${encodeURIComponent(chat_id)}/messages`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
body: JSON.stringify({ body: { contentType: "text", content: message } })
|
|
186
|
+
});
|
|
187
|
+
return { content: [{ type: "text", text: "Message sent." }] };
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
server.tool(
|
|
191
|
+
"list_teams",
|
|
192
|
+
"List the Microsoft Teams the user has joined",
|
|
193
|
+
{},
|
|
194
|
+
async () => {
|
|
195
|
+
const data = await graph("/me/joinedTeams");
|
|
196
|
+
return { content: [{ type: "text", text: JSON.stringify(data?.value ?? data, null, 2) }] };
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
server.tool(
|
|
200
|
+
"list_channels",
|
|
201
|
+
"List channels in a Microsoft Teams team",
|
|
202
|
+
{ team_id: z.string().describe("The team ID from list_teams") },
|
|
203
|
+
async ({ team_id }) => {
|
|
204
|
+
const data = await graph(`/teams/${team_id}/channels`);
|
|
205
|
+
return { content: [{ type: "text", text: JSON.stringify(data?.value ?? data, null, 2) }] };
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
server.tool(
|
|
209
|
+
"get_channel_messages",
|
|
210
|
+
"Get recent messages from a Teams channel",
|
|
211
|
+
{
|
|
212
|
+
team_id: z.string().describe("The team ID from list_teams"),
|
|
213
|
+
channel_id: z.string().describe("The channel ID from list_channels")
|
|
214
|
+
},
|
|
215
|
+
async ({ team_id, channel_id }) => {
|
|
216
|
+
const data = await graph(`/teams/${team_id}/channels/${channel_id}/messages?$top=20`);
|
|
217
|
+
return { content: [{ type: "text", text: JSON.stringify(data?.value ?? data, null, 2) }] };
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
server.tool(
|
|
221
|
+
"find_people",
|
|
222
|
+
"Search for a Microsoft Teams contact by name. Returns up to 5 matching users with their IDs, display names, and email addresses. Use the returned `id` with send_dm to send a message.",
|
|
223
|
+
{
|
|
224
|
+
query: z.string().describe("Name or partial name to search for, e.g. 'Priya' or 'Tom Baker'")
|
|
225
|
+
},
|
|
226
|
+
async ({ query }) => {
|
|
227
|
+
const encoded = encodeURIComponent(query);
|
|
228
|
+
const data = await graph(
|
|
229
|
+
`/me/people?$search=${encoded}&$select=id,displayName,userPrincipalName&$top=5`
|
|
230
|
+
);
|
|
231
|
+
const people = (data?.value ?? []).map((p) => ({
|
|
232
|
+
id: p.id,
|
|
233
|
+
displayName: p.displayName,
|
|
234
|
+
email: p.userPrincipalName ?? ""
|
|
235
|
+
}));
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: JSON.stringify(people, null, 2) }]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
server.tool(
|
|
242
|
+
"send_dm",
|
|
243
|
+
"Send a direct message to a Teams user. Call find_people first to get their user ID. Creates a new 1:1 chat if one doesn't exist yet, or reuses the existing one.",
|
|
244
|
+
{
|
|
245
|
+
user_id: z.string().describe("AAD user ID from find_people"),
|
|
246
|
+
message: z.string().describe("Plain text message to send")
|
|
247
|
+
},
|
|
248
|
+
async ({ user_id, message }) => {
|
|
249
|
+
const myId = await getMyId();
|
|
250
|
+
const chat = await graph("/chats", {
|
|
251
|
+
method: "POST",
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
chatType: "oneOnOne",
|
|
254
|
+
members: [
|
|
255
|
+
{
|
|
256
|
+
"@odata.type": "#microsoft.graph.aadUserConversationMember",
|
|
257
|
+
roles: ["owner"],
|
|
258
|
+
"user@odata.bind": `https://graph.microsoft.com/v1.0/users('${myId}')`
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"@odata.type": "#microsoft.graph.aadUserConversationMember",
|
|
262
|
+
roles: ["owner"],
|
|
263
|
+
"user@odata.bind": `https://graph.microsoft.com/v1.0/users('${user_id}')`
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
})
|
|
267
|
+
});
|
|
268
|
+
await graph(`/me/chats/${encodeURIComponent(chat.id)}/messages`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
body: JSON.stringify({ body: { contentType: "text", content: message } })
|
|
271
|
+
});
|
|
272
|
+
return { content: [{ type: "text", text: "Message sent." }] };
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
server.tool(
|
|
276
|
+
"send_channel_message",
|
|
277
|
+
"Post a message to a Teams channel",
|
|
278
|
+
{
|
|
279
|
+
team_id: z.string().describe("The team ID from list_teams"),
|
|
280
|
+
channel_id: z.string().describe("The channel ID from list_channels"),
|
|
281
|
+
message: z.string().describe("Plain text message to send")
|
|
282
|
+
},
|
|
283
|
+
async ({ team_id, channel_id, message }) => {
|
|
284
|
+
await graph(`/teams/${team_id}/channels/${channel_id}/messages`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body: JSON.stringify({ body: { contentType: "text", content: message } })
|
|
287
|
+
});
|
|
288
|
+
return { content: [{ type: "text", text: "Message sent." }] };
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
var transport = new StdioServerTransport();
|
|
292
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teamsly/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Microsoft Teams as MCP tools — DMs, channels, messages.",
|
|
5
|
+
"license": "AGPL-3.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"teamsly-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"type-check": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"zod": "^4.4.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20",
|
|
31
|
+
"tsup": "^8",
|
|
32
|
+
"typescript": "^6"
|
|
33
|
+
}
|
|
34
|
+
}
|