msteams-mcp 0.2.1
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.
Potentially problematic release.
This version of msteams-mcp might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/__fixtures__/api-responses.d.ts +254 -0
- package/dist/__fixtures__/api-responses.js +245 -0
- package/dist/api/calendar-api.d.ts +66 -0
- package/dist/api/calendar-api.js +179 -0
- package/dist/api/chatsvc-api.d.ts +352 -0
- package/dist/api/chatsvc-api.js +1100 -0
- package/dist/api/csa-api.d.ts +64 -0
- package/dist/api/csa-api.js +200 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +7 -0
- package/dist/api/substrate-api.d.ts +50 -0
- package/dist/api/substrate-api.js +305 -0
- package/dist/auth/crypto.d.ts +32 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/session-store.d.ts +87 -0
- package/dist/auth/session-store.js +230 -0
- package/dist/auth/token-extractor.d.ts +185 -0
- package/dist/auth/token-extractor.js +674 -0
- package/dist/auth/token-refresh.d.ts +25 -0
- package/dist/auth/token-refresh.js +85 -0
- package/dist/browser/auth.d.ts +53 -0
- package/dist/browser/auth.js +603 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +122 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +195 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/auth-research.d.ts +10 -0
- package/dist/research/auth-research.js +175 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +270 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +295 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +474 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +191 -0
- package/dist/tools/index.d.ts +56 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/meeting-tools.d.ts +33 -0
- package/dist/tools/meeting-tools.js +64 -0
- package/dist/tools/message-tools.d.ts +269 -0
- package/dist/tools/message-tools.js +856 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +112 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +63 -0
- package/dist/tools/search-tools.d.ts +91 -0
- package/dist/tools/search-tools.js +222 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/errors.js +132 -0
- package/dist/types/result.d.ts +43 -0
- package/dist/types/result.js +51 -0
- package/dist/types/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/dist/types/teams.d.ts +85 -0
- package/dist/types/teams.js +4 -0
- package/dist/utils/api-config.d.ts +103 -0
- package/dist/utils/api-config.js +158 -0
- package/dist/utils/auth-guards.d.ts +67 -0
- package/dist/utils/auth-guards.js +147 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +112 -0
- package/dist/utils/parsers.d.ts +247 -0
- package/dist/utils/parsers.js +731 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +511 -0
- package/package.json +62 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* People-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import type { RegisteredTool } from './index.js';
|
|
6
|
+
export declare const SearchPeopleInputSchema: z.ZodObject<{
|
|
7
|
+
query: z.ZodString;
|
|
8
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
limit: number;
|
|
11
|
+
query: string;
|
|
12
|
+
}, {
|
|
13
|
+
query: string;
|
|
14
|
+
limit?: number | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const FrequentContactsInputSchema: z.ZodObject<{
|
|
17
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
limit: number;
|
|
20
|
+
}, {
|
|
21
|
+
limit?: number | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const getMeTool: RegisteredTool<z.ZodObject<Record<string, never>>>;
|
|
24
|
+
export declare const searchPeopleTool: RegisteredTool<typeof SearchPeopleInputSchema>;
|
|
25
|
+
export declare const frequentContactsTool: RegisteredTool<typeof FrequentContactsInputSchema>;
|
|
26
|
+
/** All people-related tools. */
|
|
27
|
+
export declare const peopleTools: (RegisteredTool<z.ZodObject<Record<string, never>, z.UnknownKeysParam, z.ZodTypeAny, {
|
|
28
|
+
[x: string]: never;
|
|
29
|
+
}, {
|
|
30
|
+
[x: string]: never;
|
|
31
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
32
|
+
query: z.ZodString;
|
|
33
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
34
|
+
}, "strip", z.ZodTypeAny, {
|
|
35
|
+
limit: number;
|
|
36
|
+
query: string;
|
|
37
|
+
}, {
|
|
38
|
+
query: string;
|
|
39
|
+
limit?: number | undefined;
|
|
40
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
41
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
limit: number;
|
|
44
|
+
}, {
|
|
45
|
+
limit?: number | undefined;
|
|
46
|
+
}>>)[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* People-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { handleApiResult } from './index.js';
|
|
6
|
+
import { searchPeople, getFrequentContacts } from '../api/substrate-api.js';
|
|
7
|
+
import { getUserProfile } from '../auth/token-extractor.js';
|
|
8
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
9
|
+
import { DEFAULT_PEOPLE_LIMIT, MAX_PEOPLE_LIMIT, DEFAULT_CONTACTS_LIMIT, MAX_CONTACTS_LIMIT, } from '../constants.js';
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Schemas
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
export const SearchPeopleInputSchema = z.object({
|
|
14
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
15
|
+
limit: z.number().min(1).max(MAX_PEOPLE_LIMIT).optional().default(DEFAULT_PEOPLE_LIMIT),
|
|
16
|
+
});
|
|
17
|
+
export const FrequentContactsInputSchema = z.object({
|
|
18
|
+
limit: z.number().min(1).max(MAX_CONTACTS_LIMIT).optional().default(DEFAULT_CONTACTS_LIMIT),
|
|
19
|
+
});
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Tool Definitions
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
const getMeToolDefinition = {
|
|
24
|
+
name: 'teams_get_me',
|
|
25
|
+
description: 'Get the current user\'s profile information including email, display name, and Teams ID. Useful for finding @mentions or identifying the current user.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const searchPeopleToolDefinition = {
|
|
32
|
+
name: 'teams_search_people',
|
|
33
|
+
description: 'Search for people in Microsoft Teams by name or email. Returns matching users with display name, email, job title, and department. Useful for finding someone to message.',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
query: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Search term - can be a name, email address, or partial match',
|
|
40
|
+
},
|
|
41
|
+
limit: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: 'Maximum number of results to return (default: 10)',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['query'],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const frequentContactsToolDefinition = {
|
|
50
|
+
name: 'teams_get_frequent_contacts',
|
|
51
|
+
description: 'Get the user\'s frequently contacted people, ranked by interaction frequency. Useful for resolving ambiguous names (e.g., "Rob" → which Rob?) by checking who the user commonly works with. Returns display name, email, job title, and department.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
limit: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'Maximum number of contacts to return (default: 50)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// Handlers
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
async function handleGetMe(_input, _ctx) {
|
|
66
|
+
const profile = getUserProfile();
|
|
67
|
+
if (!profile) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: createError(ErrorCode.AUTH_REQUIRED, 'No valid session. Please use teams_login first.'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
data: { profile },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function handleSearchPeople(input, _ctx) {
|
|
79
|
+
const result = await searchPeople(input.query, input.limit);
|
|
80
|
+
return handleApiResult(result, (value) => ({
|
|
81
|
+
query: input.query,
|
|
82
|
+
returned: value.returned,
|
|
83
|
+
results: value.results,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
async function handleGetFrequentContacts(input, _ctx) {
|
|
87
|
+
const result = await getFrequentContacts(input.limit);
|
|
88
|
+
return handleApiResult(result, (value) => ({
|
|
89
|
+
returned: value.returned,
|
|
90
|
+
contacts: value.results,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Exports
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
export const getMeTool = {
|
|
97
|
+
definition: getMeToolDefinition,
|
|
98
|
+
schema: z.object({}),
|
|
99
|
+
handler: handleGetMe,
|
|
100
|
+
};
|
|
101
|
+
export const searchPeopleTool = {
|
|
102
|
+
definition: searchPeopleToolDefinition,
|
|
103
|
+
schema: SearchPeopleInputSchema,
|
|
104
|
+
handler: handleSearchPeople,
|
|
105
|
+
};
|
|
106
|
+
export const frequentContactsTool = {
|
|
107
|
+
definition: frequentContactsToolDefinition,
|
|
108
|
+
schema: FrequentContactsInputSchema,
|
|
109
|
+
handler: handleGetFrequentContacts,
|
|
110
|
+
};
|
|
111
|
+
/** All people-related tools. */
|
|
112
|
+
export const peopleTools = [getMeTool, searchPeopleTool, frequentContactsTool];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry - centralised tool management.
|
|
3
|
+
*
|
|
4
|
+
* All tools are registered here and can be looked up by name.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import type { RegisteredTool, ToolContext, ToolResult } from './index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Gets all tool definitions for MCP ListTools.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getToolDefinitions(): Tool[];
|
|
12
|
+
/**
|
|
13
|
+
* Gets a tool by name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getTool(name: string): RegisteredTool<any> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Invokes a tool by name with the given arguments and context.
|
|
18
|
+
*/
|
|
19
|
+
export declare function invokeTool(name: string, args: unknown, ctx: ToolContext): Promise<ToolResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a tool exists.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasTool(name: string): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry - centralised tool management.
|
|
3
|
+
*
|
|
4
|
+
* All tools are registered here and can be looked up by name.
|
|
5
|
+
*/
|
|
6
|
+
import { searchTools } from './search-tools.js';
|
|
7
|
+
import { messageTools } from './message-tools.js';
|
|
8
|
+
import { peopleTools } from './people-tools.js';
|
|
9
|
+
import { authTools } from './auth-tools.js';
|
|
10
|
+
import { meetingTools } from './meeting-tools.js';
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Registry
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
/** All registered tools (cast to base type for registry). */
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const allTools = [
|
|
17
|
+
...searchTools,
|
|
18
|
+
...messageTools,
|
|
19
|
+
...peopleTools,
|
|
20
|
+
...authTools,
|
|
21
|
+
...meetingTools,
|
|
22
|
+
];
|
|
23
|
+
/** Lookup map for tools by name. */
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
const toolsByName = new Map(allTools.map(tool => [tool.definition.name, tool]));
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Public API
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Gets all tool definitions for MCP ListTools.
|
|
31
|
+
*/
|
|
32
|
+
export function getToolDefinitions() {
|
|
33
|
+
return allTools.map(tool => tool.definition);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Gets a tool by name.
|
|
37
|
+
*/
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
export function getTool(name) {
|
|
40
|
+
return toolsByName.get(name);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Invokes a tool by name with the given arguments and context.
|
|
44
|
+
*/
|
|
45
|
+
export async function invokeTool(name, args, ctx) {
|
|
46
|
+
const tool = toolsByName.get(name);
|
|
47
|
+
if (!tool) {
|
|
48
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
49
|
+
}
|
|
50
|
+
// Validate input
|
|
51
|
+
const parseResult = tool.schema.safeParse(args);
|
|
52
|
+
if (!parseResult.success) {
|
|
53
|
+
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
54
|
+
}
|
|
55
|
+
// Invoke handler
|
|
56
|
+
return tool.handler(parseResult.data, ctx);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Checks if a tool exists.
|
|
60
|
+
*/
|
|
61
|
+
export function hasTool(name) {
|
|
62
|
+
return toolsByName.has(name);
|
|
63
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import type { RegisteredTool } from './index.js';
|
|
6
|
+
export declare const SearchInputSchema: z.ZodObject<{
|
|
7
|
+
query: z.ZodString;
|
|
8
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
9
|
+
from: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
10
|
+
size: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
from: number;
|
|
13
|
+
query: string;
|
|
14
|
+
maxResults: number;
|
|
15
|
+
size: number;
|
|
16
|
+
}, {
|
|
17
|
+
query: string;
|
|
18
|
+
from?: number | undefined;
|
|
19
|
+
maxResults?: number | undefined;
|
|
20
|
+
size?: number | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const GetThreadInputSchema: z.ZodObject<{
|
|
23
|
+
conversationId: z.ZodString;
|
|
24
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
25
|
+
markRead: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
26
|
+
order: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
conversationId: string;
|
|
29
|
+
limit: number;
|
|
30
|
+
markRead: boolean;
|
|
31
|
+
order: "desc" | "asc";
|
|
32
|
+
}, {
|
|
33
|
+
conversationId: string;
|
|
34
|
+
limit?: number | undefined;
|
|
35
|
+
markRead?: boolean | undefined;
|
|
36
|
+
order?: "desc" | "asc" | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const FindChannelInputSchema: z.ZodObject<{
|
|
39
|
+
query: z.ZodString;
|
|
40
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
limit: number;
|
|
43
|
+
query: string;
|
|
44
|
+
}, {
|
|
45
|
+
query: string;
|
|
46
|
+
limit?: number | undefined;
|
|
47
|
+
}>;
|
|
48
|
+
export declare const searchTool: RegisteredTool<typeof SearchInputSchema>;
|
|
49
|
+
export declare const getThreadTool: RegisteredTool<typeof GetThreadInputSchema>;
|
|
50
|
+
export declare const findChannelTool: RegisteredTool<typeof FindChannelInputSchema>;
|
|
51
|
+
/** All search-related tools. */
|
|
52
|
+
export declare const searchTools: (RegisteredTool<z.ZodObject<{
|
|
53
|
+
query: z.ZodString;
|
|
54
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
55
|
+
from: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
56
|
+
size: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
from: number;
|
|
59
|
+
query: string;
|
|
60
|
+
maxResults: number;
|
|
61
|
+
size: number;
|
|
62
|
+
}, {
|
|
63
|
+
query: string;
|
|
64
|
+
from?: number | undefined;
|
|
65
|
+
maxResults?: number | undefined;
|
|
66
|
+
size?: number | undefined;
|
|
67
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
68
|
+
conversationId: z.ZodString;
|
|
69
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
70
|
+
markRead: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
71
|
+
order: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
|
|
72
|
+
}, "strip", z.ZodTypeAny, {
|
|
73
|
+
conversationId: string;
|
|
74
|
+
limit: number;
|
|
75
|
+
markRead: boolean;
|
|
76
|
+
order: "desc" | "asc";
|
|
77
|
+
}, {
|
|
78
|
+
conversationId: string;
|
|
79
|
+
limit?: number | undefined;
|
|
80
|
+
markRead?: boolean | undefined;
|
|
81
|
+
order?: "desc" | "asc" | undefined;
|
|
82
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
83
|
+
query: z.ZodString;
|
|
84
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
85
|
+
}, "strip", z.ZodTypeAny, {
|
|
86
|
+
limit: number;
|
|
87
|
+
query: string;
|
|
88
|
+
}, {
|
|
89
|
+
query: string;
|
|
90
|
+
limit?: number | undefined;
|
|
91
|
+
}>>)[];
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { handleApiResult } from './index.js';
|
|
6
|
+
import { searchMessages, searchChannels } from '../api/substrate-api.js';
|
|
7
|
+
import { getThreadMessages, getConsumptionHorizon, markAsRead } from '../api/chatsvc-api.js';
|
|
8
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_THREAD_LIMIT, MAX_THREAD_LIMIT, DEFAULT_CHANNEL_LIMIT, MAX_CHANNEL_LIMIT, } from '../constants.js';
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Schemas
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
export const SearchInputSchema = z.object({
|
|
13
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
14
|
+
maxResults: z.number().optional().default(DEFAULT_PAGE_SIZE),
|
|
15
|
+
from: z.number().min(0).optional().default(0),
|
|
16
|
+
size: z.number().min(1).max(MAX_PAGE_SIZE).optional().default(DEFAULT_PAGE_SIZE),
|
|
17
|
+
});
|
|
18
|
+
export const GetThreadInputSchema = z.object({
|
|
19
|
+
conversationId: z.string().min(1, 'Conversation ID cannot be empty'),
|
|
20
|
+
limit: z.number().min(1).max(MAX_THREAD_LIMIT).optional().default(DEFAULT_THREAD_LIMIT),
|
|
21
|
+
markRead: z.boolean().optional().default(false),
|
|
22
|
+
order: z.enum(['asc', 'desc']).optional().default('desc'),
|
|
23
|
+
});
|
|
24
|
+
export const FindChannelInputSchema = z.object({
|
|
25
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
26
|
+
limit: z.number().min(1).max(MAX_CHANNEL_LIMIT).optional().default(DEFAULT_CHANNEL_LIMIT),
|
|
27
|
+
});
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Tool Definitions
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
const searchToolDefinition = {
|
|
32
|
+
name: 'teams_search',
|
|
33
|
+
description: 'Search for messages in Microsoft Teams. Returns matching messages with sender, timestamp, content, conversationId (for replies), and pagination info. Supports operators: from:email, to:name, sent:YYYY-MM-DD, sent:today, is:Messages, is:Meetings, is:Channels, is:Chats, hasattachment:true, "Name" for @mentions. The in:channel operator only works reliably when combined with content (e.g., "meeting in:IT Support"). Combine with NOT to exclude. Results sorted by recency.',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
query: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Search query with optional operators. WORKING: from:email (or name), to:name (spaces not dots - "to:rob macdonald"), sent:YYYY-MM-DD, sent:>=YYYY-MM-DD, sent:today, is:Messages, is:Meetings, is:Channels, is:Chats (case-sensitive, plural required), hasattachment:true, "Display Name" for @mentions. CHANNEL FILTER: in:channel only works reliably WITH content terms (e.g., "budget in:IT Support"). NEVER quote channel names. NOT WORKING: mentions:, sent:lastweek, @me, from:me, is:meeting (must be is:Meetings). Use teams_get_me first to get email/displayName.',
|
|
40
|
+
},
|
|
41
|
+
maxResults: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: 'Maximum number of results to return (default: 25)',
|
|
44
|
+
},
|
|
45
|
+
from: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'Starting offset for pagination (0-based, default: 0). Use this to get subsequent pages of results.',
|
|
48
|
+
},
|
|
49
|
+
size: {
|
|
50
|
+
type: 'number',
|
|
51
|
+
description: 'Page size (default: 25). Number of results per page.',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ['query'],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const getThreadToolDefinition = {
|
|
58
|
+
name: 'teams_get_thread',
|
|
59
|
+
description: 'Get messages from a Teams conversation/thread. Default: newest-first (latest messages at top). For channels: messages include isThreadReply (true for replies) and threadRootId (ID of the post being replied to). Messages without threadRootId are top-level posts. Use threadRootId to group related messages. Each message includes a "when" field with the day of week (e.g., "Friday, January 30, 2026, 10:45 AM UTC"). Returns unread count and can optionally mark as read.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
conversationId: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'The conversation ID to get messages from (e.g., "19:abc@thread.tacv2" from search results)',
|
|
66
|
+
},
|
|
67
|
+
limit: {
|
|
68
|
+
type: 'number',
|
|
69
|
+
description: 'Maximum number of messages to return (default: 50, max: 200)',
|
|
70
|
+
},
|
|
71
|
+
markRead: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
description: 'If true, marks the conversation as read up to the latest message after fetching (default: false)',
|
|
74
|
+
},
|
|
75
|
+
order: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: ['asc', 'desc'],
|
|
78
|
+
description: 'Sort order: "desc" (newest-first, default) or "asc" (oldest-first for chronological reading)',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['conversationId'],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const findChannelToolDefinition = {
|
|
85
|
+
name: 'teams_find_channel',
|
|
86
|
+
description: 'Find Teams channels by name. Searches both (1) channels in teams you\'re a member of (reliable) and (2) channels across the organisation (discovery). Results indicate whether you\'re already a member via the isMember field. Channel IDs can be used with teams_get_thread to read messages.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
query: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Channel name to search for (partial match)',
|
|
93
|
+
},
|
|
94
|
+
limit: {
|
|
95
|
+
type: 'number',
|
|
96
|
+
description: 'Maximum number of results (default: 10, max: 50)',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ['query'],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Handlers
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
async function handleSearch(input, _ctx) {
|
|
106
|
+
const result = await searchMessages(input.query, {
|
|
107
|
+
maxResults: input.maxResults,
|
|
108
|
+
from: input.from,
|
|
109
|
+
size: input.size,
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
return { success: false, error: result.error };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
data: {
|
|
117
|
+
query: input.query,
|
|
118
|
+
resultCount: result.value.results.length,
|
|
119
|
+
pagination: {
|
|
120
|
+
from: result.value.pagination.from,
|
|
121
|
+
size: result.value.pagination.size,
|
|
122
|
+
returned: result.value.pagination.returned,
|
|
123
|
+
total: result.value.pagination.total,
|
|
124
|
+
hasMore: result.value.pagination.hasMore,
|
|
125
|
+
nextFrom: result.value.pagination.hasMore
|
|
126
|
+
? result.value.pagination.from + result.value.pagination.returned
|
|
127
|
+
: undefined,
|
|
128
|
+
},
|
|
129
|
+
results: result.value.results,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function handleGetThread(input, _ctx) {
|
|
134
|
+
const result = await getThreadMessages(input.conversationId, {
|
|
135
|
+
limit: input.limit,
|
|
136
|
+
order: input.order,
|
|
137
|
+
});
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
return { success: false, error: result.error };
|
|
140
|
+
}
|
|
141
|
+
// Get unread status
|
|
142
|
+
let unreadCount;
|
|
143
|
+
let lastReadMessageId;
|
|
144
|
+
const horizonResult = await getConsumptionHorizon(input.conversationId);
|
|
145
|
+
if (horizonResult.ok) {
|
|
146
|
+
lastReadMessageId = horizonResult.value.lastReadMessageId;
|
|
147
|
+
// Count messages after last read
|
|
148
|
+
if (lastReadMessageId) {
|
|
149
|
+
let foundLastRead = false;
|
|
150
|
+
unreadCount = 0;
|
|
151
|
+
for (const msg of result.value.messages) {
|
|
152
|
+
if (msg.id === lastReadMessageId) {
|
|
153
|
+
foundLastRead = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (foundLastRead && !msg.isFromMe) {
|
|
157
|
+
unreadCount++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// If last read message wasn't found, it's older than our window - all messages are unread
|
|
161
|
+
if (!foundLastRead) {
|
|
162
|
+
unreadCount = result.value.messages.filter(m => !m.isFromMe).length;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// No consumption horizon means all messages are unread (new conversation)
|
|
167
|
+
unreadCount = result.value.messages.filter(m => !m.isFromMe).length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Mark as read if requested
|
|
171
|
+
let markedAsRead = false;
|
|
172
|
+
if (input.markRead && result.value.messages.length > 0) {
|
|
173
|
+
// Find the latest message - depends on sort order
|
|
174
|
+
// desc (default): newest is first [0], asc: newest is last [-1]
|
|
175
|
+
const latestMessage = input.order === 'asc'
|
|
176
|
+
? result.value.messages[result.value.messages.length - 1]
|
|
177
|
+
: result.value.messages[0];
|
|
178
|
+
if (latestMessage) {
|
|
179
|
+
const markResult = await markAsRead(input.conversationId, latestMessage.id);
|
|
180
|
+
markedAsRead = markResult.ok;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
data: {
|
|
186
|
+
conversationId: result.value.conversationId,
|
|
187
|
+
messageCount: result.value.messages.length,
|
|
188
|
+
unreadCount,
|
|
189
|
+
lastReadMessageId,
|
|
190
|
+
markedAsRead: input.markRead ? markedAsRead : undefined,
|
|
191
|
+
messages: result.value.messages,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function handleFindChannel(input, _ctx) {
|
|
196
|
+
const result = await searchChannels(input.query, input.limit);
|
|
197
|
+
return handleApiResult(result, (value) => ({
|
|
198
|
+
query: input.query,
|
|
199
|
+
count: value.returned,
|
|
200
|
+
channels: value.results,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
// Exports
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
export const searchTool = {
|
|
207
|
+
definition: searchToolDefinition,
|
|
208
|
+
schema: SearchInputSchema,
|
|
209
|
+
handler: handleSearch,
|
|
210
|
+
};
|
|
211
|
+
export const getThreadTool = {
|
|
212
|
+
definition: getThreadToolDefinition,
|
|
213
|
+
schema: GetThreadInputSchema,
|
|
214
|
+
handler: handleGetThread,
|
|
215
|
+
};
|
|
216
|
+
export const findChannelTool = {
|
|
217
|
+
definition: findChannelToolDefinition,
|
|
218
|
+
schema: FindChannelInputSchema,
|
|
219
|
+
handler: handleFindChannel,
|
|
220
|
+
};
|
|
221
|
+
/** All search-related tools. */
|
|
222
|
+
export const searchTools = [searchTool, getThreadTool, findChannelTool];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy for MCP operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides machine-readable error codes that help LLMs
|
|
5
|
+
* understand failures and take appropriate action.
|
|
6
|
+
*/
|
|
7
|
+
/** Enumeration of all error types in the system. */
|
|
8
|
+
export declare enum ErrorCode {
|
|
9
|
+
/** No valid authentication token or session. */
|
|
10
|
+
AUTH_REQUIRED = "AUTH_REQUIRED",
|
|
11
|
+
/** Token has expired and needs refresh. */
|
|
12
|
+
AUTH_EXPIRED = "AUTH_EXPIRED",
|
|
13
|
+
/** Rate limited by the API. */
|
|
14
|
+
RATE_LIMITED = "RATE_LIMITED",
|
|
15
|
+
/** Requested resource was not found. */
|
|
16
|
+
NOT_FOUND = "NOT_FOUND",
|
|
17
|
+
/** Invalid input parameters. */
|
|
18
|
+
INVALID_INPUT = "INVALID_INPUT",
|
|
19
|
+
/** API returned an error response. */
|
|
20
|
+
API_ERROR = "API_ERROR",
|
|
21
|
+
/** Browser automation failed. */
|
|
22
|
+
BROWSER_ERROR = "BROWSER_ERROR",
|
|
23
|
+
/** Network or connection error. */
|
|
24
|
+
NETWORK_ERROR = "NETWORK_ERROR",
|
|
25
|
+
/** Operation timed out. */
|
|
26
|
+
TIMEOUT = "TIMEOUT",
|
|
27
|
+
/** Unknown or unexpected error. */
|
|
28
|
+
UNKNOWN = "UNKNOWN"
|
|
29
|
+
}
|
|
30
|
+
/** Structured error with machine-readable information. */
|
|
31
|
+
export interface McpError {
|
|
32
|
+
/** Machine-readable error code. */
|
|
33
|
+
code: ErrorCode;
|
|
34
|
+
/** Human-readable error message. */
|
|
35
|
+
message: string;
|
|
36
|
+
/** Whether this error is potentially transient and retryable. */
|
|
37
|
+
retryable: boolean;
|
|
38
|
+
/** Suggested wait time before retry (milliseconds). */
|
|
39
|
+
retryAfterMs?: number;
|
|
40
|
+
/** Suggestions for resolving the error (for LLMs). */
|
|
41
|
+
suggestions: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a standardised MCP error.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createError(code: ErrorCode, message: string, options?: {
|
|
47
|
+
retryable?: boolean;
|
|
48
|
+
retryAfterMs?: number;
|
|
49
|
+
suggestions?: string[];
|
|
50
|
+
}): McpError;
|
|
51
|
+
/**
|
|
52
|
+
* Classifies an HTTP status code into an error code.
|
|
53
|
+
*/
|
|
54
|
+
export declare function classifyHttpError(status: number, message?: string): ErrorCode;
|
|
55
|
+
/**
|
|
56
|
+
* Extracts retry-after value from response headers.
|
|
57
|
+
*/
|
|
58
|
+
export declare function extractRetryAfter(headers: Headers): number | undefined;
|