@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.
Files changed (3) hide show
  1. package/README.md +160 -0
  2. package/dist/index.js +292 -0
  3. 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
+ }