msteams-mcp 0.2.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 +229 -0
- package/dist/__fixtures__/api-responses.d.ts +228 -0
- package/dist/__fixtures__/api-responses.js +217 -0
- package/dist/api/chatsvc-api.d.ts +171 -0
- package/dist/api/chatsvc-api.js +459 -0
- package/dist/api/csa-api.d.ts +44 -0
- package/dist/api/csa-api.js +148 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.js +6 -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 +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/session-store.d.ts +82 -0
- package/dist/auth/session-store.js +136 -0
- package/dist/auth/token-extractor.d.ts +69 -0
- package/dist/auth/token-extractor.js +330 -0
- package/dist/browser/auth.d.ts +43 -0
- package/dist/browser/auth.js +232 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +121 -0
- package/dist/browser/session.d.ts +34 -0
- package/dist/browser/session.js +92 -0
- package/dist/constants.d.ts +54 -0
- package/dist/constants.js +72 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +267 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +291 -0
- package/dist/teams/api-interceptor.d.ts +54 -0
- package/dist/teams/api-interceptor.js +391 -0
- package/dist/teams/direct-api.d.ts +321 -0
- package/dist/teams/direct-api.js +1305 -0
- package/dist/teams/messages.d.ts +14 -0
- package/dist/teams/messages.js +142 -0
- package/dist/teams/search.d.ts +40 -0
- package/dist/teams/search.js +458 -0
- package/dist/test/cli.d.ts +12 -0
- package/dist/test/cli.js +328 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/manual-test.d.ts +11 -0
- package/dist/test/manual-test.js +160 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +427 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +127 -0
- package/dist/tools/index.d.ts +45 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/message-tools.d.ts +139 -0
- package/dist/tools/message-tools.js +433 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +123 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +61 -0
- package/dist/tools/search-tools.d.ts +79 -0
- package/dist/tools/search-tools.js +168 -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/teams.d.ts +79 -0
- package/dist/types/teams.js +5 -0
- package/dist/utils/api-config.d.ts +66 -0
- package/dist/utils/api-config.js +113 -0
- package/dist/utils/auth-guards.d.ts +29 -0
- package/dist/utils/auth-guards.js +54 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +111 -0
- package/dist/utils/parsers.d.ts +187 -0
- package/dist/utils/parsers.js +574 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +360 -0
- package/package.json +58 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* People-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { searchPeople, getFrequentContacts } from '../api/substrate-api.js';
|
|
6
|
+
import { getUserProfile } from '../auth/token-extractor.js';
|
|
7
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
8
|
+
import { DEFAULT_PEOPLE_LIMIT, MAX_PEOPLE_LIMIT, DEFAULT_CONTACTS_LIMIT, MAX_CONTACTS_LIMIT, } from '../constants.js';
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Schemas
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
export const SearchPeopleInputSchema = z.object({
|
|
13
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
14
|
+
limit: z.number().min(1).max(MAX_PEOPLE_LIMIT).optional().default(DEFAULT_PEOPLE_LIMIT),
|
|
15
|
+
});
|
|
16
|
+
export const FrequentContactsInputSchema = z.object({
|
|
17
|
+
limit: z.number().min(1).max(MAX_CONTACTS_LIMIT).optional().default(DEFAULT_CONTACTS_LIMIT),
|
|
18
|
+
});
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Tool Definitions
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
const getMeToolDefinition = {
|
|
23
|
+
name: 'teams_get_me',
|
|
24
|
+
description: 'Get the current user\'s profile information including email, display name, and Teams ID. Useful for finding @mentions or identifying the current user.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const searchPeopleToolDefinition = {
|
|
31
|
+
name: 'teams_search_people',
|
|
32
|
+
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.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
query: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Search term - can be a name, email address, or partial match',
|
|
39
|
+
},
|
|
40
|
+
limit: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: 'Maximum number of results to return (default: 10)',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['query'],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const frequentContactsToolDefinition = {
|
|
49
|
+
name: 'teams_get_frequent_contacts',
|
|
50
|
+
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.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
limit: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: 'Maximum number of contacts to return (default: 50)',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Handlers
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
async function handleGetMe(_input, _ctx) {
|
|
65
|
+
const profile = getUserProfile();
|
|
66
|
+
if (!profile) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: createError(ErrorCode.AUTH_REQUIRED, 'No valid session. Please use teams_login first.'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: { profile },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function handleSearchPeople(input, _ctx) {
|
|
78
|
+
const result = await searchPeople(input.query, input.limit);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
return { success: false, error: result.error };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
query: input.query,
|
|
86
|
+
returned: result.value.returned,
|
|
87
|
+
results: result.value.results,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function handleGetFrequentContacts(input, _ctx) {
|
|
92
|
+
const result = await getFrequentContacts(input.limit);
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return { success: false, error: result.error };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
data: {
|
|
99
|
+
returned: result.value.returned,
|
|
100
|
+
contacts: result.value.results,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
// Exports
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
export const getMeTool = {
|
|
108
|
+
definition: getMeToolDefinition,
|
|
109
|
+
schema: z.object({}),
|
|
110
|
+
handler: handleGetMe,
|
|
111
|
+
};
|
|
112
|
+
export const searchPeopleTool = {
|
|
113
|
+
definition: searchPeopleToolDefinition,
|
|
114
|
+
schema: SearchPeopleInputSchema,
|
|
115
|
+
handler: handleSearchPeople,
|
|
116
|
+
};
|
|
117
|
+
export const frequentContactsTool = {
|
|
118
|
+
definition: frequentContactsToolDefinition,
|
|
119
|
+
schema: FrequentContactsInputSchema,
|
|
120
|
+
handler: handleGetFrequentContacts,
|
|
121
|
+
};
|
|
122
|
+
/** All people-related tools. */
|
|
123
|
+
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,61 @@
|
|
|
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
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Registry
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
/** All registered tools (cast to base type for registry). */
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const allTools = [
|
|
16
|
+
...searchTools,
|
|
17
|
+
...messageTools,
|
|
18
|
+
...peopleTools,
|
|
19
|
+
...authTools,
|
|
20
|
+
];
|
|
21
|
+
/** Lookup map for tools by name. */
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const toolsByName = new Map(allTools.map(tool => [tool.definition.name, tool]));
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Public API
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Gets all tool definitions for MCP ListTools.
|
|
29
|
+
*/
|
|
30
|
+
export function getToolDefinitions() {
|
|
31
|
+
return allTools.map(tool => tool.definition);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets a tool by name.
|
|
35
|
+
*/
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
export function getTool(name) {
|
|
38
|
+
return toolsByName.get(name);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Invokes a tool by name with the given arguments and context.
|
|
42
|
+
*/
|
|
43
|
+
export async function invokeTool(name, args, ctx) {
|
|
44
|
+
const tool = toolsByName.get(name);
|
|
45
|
+
if (!tool) {
|
|
46
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
// Validate input
|
|
49
|
+
const parseResult = tool.schema.safeParse(args);
|
|
50
|
+
if (!parseResult.success) {
|
|
51
|
+
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
52
|
+
}
|
|
53
|
+
// Invoke handler
|
|
54
|
+
return tool.handler(parseResult.data, ctx);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a tool exists.
|
|
58
|
+
*/
|
|
59
|
+
export function hasTool(name) {
|
|
60
|
+
return toolsByName.has(name);
|
|
61
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
conversationId: string;
|
|
27
|
+
limit: number;
|
|
28
|
+
}, {
|
|
29
|
+
conversationId: string;
|
|
30
|
+
limit?: number | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
export declare const FindChannelInputSchema: z.ZodObject<{
|
|
33
|
+
query: z.ZodString;
|
|
34
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
query: string;
|
|
37
|
+
limit: number;
|
|
38
|
+
}, {
|
|
39
|
+
query: string;
|
|
40
|
+
limit?: number | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
export declare const searchTool: RegisteredTool<typeof SearchInputSchema>;
|
|
43
|
+
export declare const getThreadTool: RegisteredTool<typeof GetThreadInputSchema>;
|
|
44
|
+
export declare const findChannelTool: RegisteredTool<typeof FindChannelInputSchema>;
|
|
45
|
+
/** All search-related tools. */
|
|
46
|
+
export declare const searchTools: (RegisteredTool<z.ZodObject<{
|
|
47
|
+
query: z.ZodString;
|
|
48
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
49
|
+
from: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
50
|
+
size: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
from: number;
|
|
53
|
+
query: string;
|
|
54
|
+
maxResults: number;
|
|
55
|
+
size: number;
|
|
56
|
+
}, {
|
|
57
|
+
query: string;
|
|
58
|
+
from?: number | undefined;
|
|
59
|
+
maxResults?: number | undefined;
|
|
60
|
+
size?: number | undefined;
|
|
61
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
62
|
+
conversationId: z.ZodString;
|
|
63
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
64
|
+
}, "strip", z.ZodTypeAny, {
|
|
65
|
+
conversationId: string;
|
|
66
|
+
limit: number;
|
|
67
|
+
}, {
|
|
68
|
+
conversationId: string;
|
|
69
|
+
limit?: number | undefined;
|
|
70
|
+
}>> | RegisteredTool<z.ZodObject<{
|
|
71
|
+
query: z.ZodString;
|
|
72
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
73
|
+
}, "strip", z.ZodTypeAny, {
|
|
74
|
+
query: string;
|
|
75
|
+
limit: number;
|
|
76
|
+
}, {
|
|
77
|
+
query: string;
|
|
78
|
+
limit?: number | undefined;
|
|
79
|
+
}>>)[];
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-related tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { searchMessages, searchChannels } from '../api/substrate-api.js';
|
|
6
|
+
import { getThreadMessages } from '../api/chatsvc-api.js';
|
|
7
|
+
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_THREAD_LIMIT, MAX_THREAD_LIMIT, DEFAULT_CHANNEL_LIMIT, MAX_CHANNEL_LIMIT, } from '../constants.js';
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Schemas
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
export const SearchInputSchema = z.object({
|
|
12
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
13
|
+
maxResults: z.number().optional().default(DEFAULT_PAGE_SIZE),
|
|
14
|
+
from: z.number().min(0).optional().default(0),
|
|
15
|
+
size: z.number().min(1).max(MAX_PAGE_SIZE).optional().default(DEFAULT_PAGE_SIZE),
|
|
16
|
+
});
|
|
17
|
+
export const GetThreadInputSchema = z.object({
|
|
18
|
+
conversationId: z.string().min(1, 'Conversation ID cannot be empty'),
|
|
19
|
+
limit: z.number().min(1).max(MAX_THREAD_LIMIT).optional().default(DEFAULT_THREAD_LIMIT),
|
|
20
|
+
});
|
|
21
|
+
export const FindChannelInputSchema = z.object({
|
|
22
|
+
query: z.string().min(1, 'Query cannot be empty'),
|
|
23
|
+
limit: z.number().min(1).max(MAX_CHANNEL_LIMIT).optional().default(DEFAULT_CHANNEL_LIMIT),
|
|
24
|
+
});
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Tool Definitions
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
const searchToolDefinition = {
|
|
29
|
+
name: 'teams_search',
|
|
30
|
+
description: 'Search for messages in Microsoft Teams. Returns matching messages with sender, timestamp, content, conversationId (for replies), and pagination info. Supports search operators: from:email, sent:today/lastweek, in:channel, hasattachment:true, "Name" for @mentions. Combine with NOT to exclude (e.g., NOT from:rob@co.com).',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
query: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Search query with optional operators. Examples: "budget report", "from:sarah@co.com sent:lastweek", "\"Rob Smith\" NOT from:rob@co.com" (find @mentions of Rob). IMPORTANT: "@me", "from:me", "to:me" do NOT work - use teams_get_me first to get actual email/displayName, then use those values.',
|
|
37
|
+
},
|
|
38
|
+
maxResults: {
|
|
39
|
+
type: 'number',
|
|
40
|
+
description: 'Maximum number of results to return (default: 25)',
|
|
41
|
+
},
|
|
42
|
+
from: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
description: 'Starting offset for pagination (0-based, default: 0). Use this to get subsequent pages of results.',
|
|
45
|
+
},
|
|
46
|
+
size: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
description: 'Page size (default: 25). Number of results per page.',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ['query'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const getThreadToolDefinition = {
|
|
55
|
+
name: 'teams_get_thread',
|
|
56
|
+
description: 'Get messages from a Teams conversation/thread. Use this to see replies to a message, check thread context, or read recent messages in a chat. Requires a conversationId (available from search results).',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
conversationId: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'The conversation ID to get messages from (e.g., "19:abc@thread.tacv2" from search results)',
|
|
63
|
+
},
|
|
64
|
+
limit: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
description: 'Maximum number of messages to return (default: 50, max: 200)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['conversationId'],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const findChannelToolDefinition = {
|
|
73
|
+
name: 'teams_find_channel',
|
|
74
|
+
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.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
query: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Channel name to search for (partial match)',
|
|
81
|
+
},
|
|
82
|
+
limit: {
|
|
83
|
+
type: 'number',
|
|
84
|
+
description: 'Maximum number of results (default: 10, max: 50)',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ['query'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// Handlers
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
async function handleSearch(input, _ctx) {
|
|
94
|
+
const result = await searchMessages(input.query, {
|
|
95
|
+
maxResults: input.maxResults,
|
|
96
|
+
from: input.from,
|
|
97
|
+
size: input.size,
|
|
98
|
+
});
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
return { success: false, error: result.error };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
data: {
|
|
105
|
+
query: input.query,
|
|
106
|
+
resultCount: result.value.results.length,
|
|
107
|
+
pagination: {
|
|
108
|
+
from: result.value.pagination.from,
|
|
109
|
+
size: result.value.pagination.size,
|
|
110
|
+
returned: result.value.pagination.returned,
|
|
111
|
+
total: result.value.pagination.total,
|
|
112
|
+
hasMore: result.value.pagination.hasMore,
|
|
113
|
+
nextFrom: result.value.pagination.hasMore
|
|
114
|
+
? result.value.pagination.from + result.value.pagination.returned
|
|
115
|
+
: undefined,
|
|
116
|
+
},
|
|
117
|
+
results: result.value.results,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function handleGetThread(input, _ctx) {
|
|
122
|
+
const result = await getThreadMessages(input.conversationId, { limit: input.limit });
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
return { success: false, error: result.error };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
data: {
|
|
129
|
+
conversationId: result.value.conversationId,
|
|
130
|
+
messageCount: result.value.messages.length,
|
|
131
|
+
messages: result.value.messages,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function handleFindChannel(input, _ctx) {
|
|
136
|
+
const result = await searchChannels(input.query, input.limit);
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
return { success: false, error: result.error };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
data: {
|
|
143
|
+
query: input.query,
|
|
144
|
+
count: result.value.returned,
|
|
145
|
+
channels: result.value.results,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// Exports
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
export const searchTool = {
|
|
153
|
+
definition: searchToolDefinition,
|
|
154
|
+
schema: SearchInputSchema,
|
|
155
|
+
handler: handleSearch,
|
|
156
|
+
};
|
|
157
|
+
export const getThreadTool = {
|
|
158
|
+
definition: getThreadToolDefinition,
|
|
159
|
+
schema: GetThreadInputSchema,
|
|
160
|
+
handler: handleGetThread,
|
|
161
|
+
};
|
|
162
|
+
export const findChannelTool = {
|
|
163
|
+
definition: findChannelToolDefinition,
|
|
164
|
+
schema: FindChannelInputSchema,
|
|
165
|
+
handler: handleFindChannel,
|
|
166
|
+
};
|
|
167
|
+
/** All search-related tools. */
|
|
168
|
+
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;
|
|
@@ -0,0 +1,132 @@
|
|
|
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 var ErrorCode;
|
|
9
|
+
(function (ErrorCode) {
|
|
10
|
+
/** No valid authentication token or session. */
|
|
11
|
+
ErrorCode["AUTH_REQUIRED"] = "AUTH_REQUIRED";
|
|
12
|
+
/** Token has expired and needs refresh. */
|
|
13
|
+
ErrorCode["AUTH_EXPIRED"] = "AUTH_EXPIRED";
|
|
14
|
+
/** Rate limited by the API. */
|
|
15
|
+
ErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
|
|
16
|
+
/** Requested resource was not found. */
|
|
17
|
+
ErrorCode["NOT_FOUND"] = "NOT_FOUND";
|
|
18
|
+
/** Invalid input parameters. */
|
|
19
|
+
ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
|
|
20
|
+
/** API returned an error response. */
|
|
21
|
+
ErrorCode["API_ERROR"] = "API_ERROR";
|
|
22
|
+
/** Browser automation failed. */
|
|
23
|
+
ErrorCode["BROWSER_ERROR"] = "BROWSER_ERROR";
|
|
24
|
+
/** Network or connection error. */
|
|
25
|
+
ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
26
|
+
/** Operation timed out. */
|
|
27
|
+
ErrorCode["TIMEOUT"] = "TIMEOUT";
|
|
28
|
+
/** Unknown or unexpected error. */
|
|
29
|
+
ErrorCode["UNKNOWN"] = "UNKNOWN";
|
|
30
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
31
|
+
/**
|
|
32
|
+
* Creates a standardised MCP error.
|
|
33
|
+
*/
|
|
34
|
+
export function createError(code, message, options = {}) {
|
|
35
|
+
const defaultSuggestions = getDefaultSuggestions(code);
|
|
36
|
+
return {
|
|
37
|
+
code,
|
|
38
|
+
message,
|
|
39
|
+
retryable: options.retryable ?? isRetryableByDefault(code),
|
|
40
|
+
retryAfterMs: options.retryAfterMs,
|
|
41
|
+
suggestions: options.suggestions ?? defaultSuggestions,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns default suggestions for each error code.
|
|
46
|
+
*/
|
|
47
|
+
function getDefaultSuggestions(code) {
|
|
48
|
+
switch (code) {
|
|
49
|
+
case ErrorCode.AUTH_REQUIRED:
|
|
50
|
+
return ['Call teams_login to authenticate'];
|
|
51
|
+
case ErrorCode.AUTH_EXPIRED:
|
|
52
|
+
return ['Call teams_login to refresh authentication'];
|
|
53
|
+
case ErrorCode.RATE_LIMITED:
|
|
54
|
+
return ['Wait before retrying', 'Reduce request frequency'];
|
|
55
|
+
case ErrorCode.NOT_FOUND:
|
|
56
|
+
return ['Check the ID/query is correct', 'Verify the resource exists'];
|
|
57
|
+
case ErrorCode.INVALID_INPUT:
|
|
58
|
+
return ['Check the input parameters', 'Review the tool documentation'];
|
|
59
|
+
case ErrorCode.API_ERROR:
|
|
60
|
+
return ['Retry the request', 'Check teams_status for system health'];
|
|
61
|
+
case ErrorCode.BROWSER_ERROR:
|
|
62
|
+
return ['Call teams_login to restart browser session'];
|
|
63
|
+
case ErrorCode.NETWORK_ERROR:
|
|
64
|
+
return ['Check network connectivity', 'Retry the request'];
|
|
65
|
+
case ErrorCode.TIMEOUT:
|
|
66
|
+
return ['Retry the request', 'Use smaller page sizes'];
|
|
67
|
+
case ErrorCode.UNKNOWN:
|
|
68
|
+
return ['Check teams_status', 'Try teams_login if authentication issues'];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Determines if an error code is retryable by default.
|
|
73
|
+
*/
|
|
74
|
+
function isRetryableByDefault(code) {
|
|
75
|
+
switch (code) {
|
|
76
|
+
case ErrorCode.RATE_LIMITED:
|
|
77
|
+
case ErrorCode.NETWORK_ERROR:
|
|
78
|
+
case ErrorCode.TIMEOUT:
|
|
79
|
+
case ErrorCode.API_ERROR:
|
|
80
|
+
return true;
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Classifies an HTTP status code into an error code.
|
|
87
|
+
*/
|
|
88
|
+
export function classifyHttpError(status, message) {
|
|
89
|
+
switch (status) {
|
|
90
|
+
case 401:
|
|
91
|
+
return ErrorCode.AUTH_EXPIRED;
|
|
92
|
+
case 403:
|
|
93
|
+
return ErrorCode.AUTH_REQUIRED;
|
|
94
|
+
case 404:
|
|
95
|
+
return ErrorCode.NOT_FOUND;
|
|
96
|
+
case 429:
|
|
97
|
+
return ErrorCode.RATE_LIMITED;
|
|
98
|
+
case 400:
|
|
99
|
+
case 422:
|
|
100
|
+
return ErrorCode.INVALID_INPUT;
|
|
101
|
+
default:
|
|
102
|
+
if (status >= 500)
|
|
103
|
+
return ErrorCode.API_ERROR;
|
|
104
|
+
if (message?.includes('timeout'))
|
|
105
|
+
return ErrorCode.TIMEOUT;
|
|
106
|
+
if (message?.includes('network') || message?.includes('ECONNRESET')) {
|
|
107
|
+
return ErrorCode.NETWORK_ERROR;
|
|
108
|
+
}
|
|
109
|
+
return ErrorCode.UNKNOWN;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Extracts retry-after value from response headers.
|
|
114
|
+
*/
|
|
115
|
+
export function extractRetryAfter(headers) {
|
|
116
|
+
const retryAfter = headers.get('Retry-After');
|
|
117
|
+
if (!retryAfter)
|
|
118
|
+
return undefined;
|
|
119
|
+
// Could be seconds (number) or HTTP date
|
|
120
|
+
const seconds = parseInt(retryAfter, 10);
|
|
121
|
+
if (!isNaN(seconds)) {
|
|
122
|
+
return seconds * 1000;
|
|
123
|
+
}
|
|
124
|
+
// Try parsing as HTTP date
|
|
125
|
+
try {
|
|
126
|
+
const date = new Date(retryAfter);
|
|
127
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|