msteams-mcp 0.29.2 → 0.30.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.
- package/README.md +21 -15
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +10 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/browser/auth.js +6 -4
- package/dist/browser/auth.test.js +20 -1
- package/dist/cli/help.d.ts +21 -0
- package/dist/cli/help.js +111 -0
- package/dist/cli/render.d.ts +21 -0
- package/dist/cli/render.js +155 -0
- package/dist/cli/resolve.d.ts +50 -0
- package/dist/cli/resolve.js +197 -0
- package/dist/cli/resolve.test.d.ts +8 -0
- package/dist/cli/resolve.test.js +118 -0
- package/dist/cli.d.ts +12 -12
- package/dist/cli.js +93 -450
- package/dist/index.js +0 -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 +287 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -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 +476 -0
- package/dist/tools/message-tools.js +3 -3
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.js +12 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -2
- package/dist/utils/logger.test.js +28 -53
- package/dist/utils/parsers-markdown.d.ts +19 -12
- package/dist/utils/parsers-markdown.js +153 -95
- package/dist/utils/parsers.test.js +56 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ This installs two binaries: `msteams-mcp` (the MCP server) and `msteams` (the CL
|
|
|
102
102
|
npx -y msteams-mcp msteams status
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
See [CLI Usage](#cli-usage) for commands.
|
|
105
|
+
See [CLI Usage](#cli-usage) for commands, or run `msteams` for the command list.
|
|
106
106
|
|
|
107
107
|
## Available Tools
|
|
108
108
|
|
|
@@ -268,13 +268,18 @@ The server also exposes passive resources for context discovery:
|
|
|
268
268
|
|
|
269
269
|
## CLI Usage
|
|
270
270
|
|
|
271
|
-
`msteams` exposes every tool the MCP server does - full parity, same authentication, same session files.
|
|
271
|
+
`msteams` exposes every tool the MCP server does - full parity, same authentication, same session files. Commands, flags and help are generated from the tool definitions, so the CLI never lags behind the server.
|
|
272
272
|
|
|
273
273
|
If you installed globally (`npm install -g msteams-mcp`), invoke it directly:
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
#
|
|
276
|
+
# The command list, one line each
|
|
277
277
|
msteams
|
|
278
|
+
msteams --version
|
|
279
|
+
|
|
280
|
+
# What one command does, and all of its flags
|
|
281
|
+
msteams help send_message
|
|
282
|
+
msteams search --help
|
|
278
283
|
|
|
279
284
|
# Check authentication status
|
|
280
285
|
msteams status
|
|
@@ -288,9 +293,9 @@ msteams search "meeting notes"
|
|
|
288
293
|
msteams search "project" --from 0 --size 50
|
|
289
294
|
|
|
290
295
|
# Search emails
|
|
291
|
-
msteams
|
|
296
|
+
msteams email "from:sarah@company.com"
|
|
292
297
|
|
|
293
|
-
# Send a message (default: your own
|
|
298
|
+
# Send a message (default: your own self-chat, 48:notes)
|
|
294
299
|
msteams send "Hello from Teams MCP!"
|
|
295
300
|
msteams send "Message" --to "conversation-id"
|
|
296
301
|
|
|
@@ -300,29 +305,30 @@ msteams favorites
|
|
|
300
305
|
msteams activity
|
|
301
306
|
msteams unread
|
|
302
307
|
|
|
303
|
-
#
|
|
304
|
-
msteams
|
|
305
|
-
msteams find_channel --query "support"
|
|
306
|
-
|
|
307
|
-
# Machine-readable output
|
|
308
|
-
msteams search "query" --json
|
|
308
|
+
# Machine-readable output, on stdout, for scripts
|
|
309
|
+
msteams search "query" --json | jq '.results[0].conversationId'
|
|
309
310
|
```
|
|
310
311
|
|
|
311
|
-
**Command form:** `msteams <command> [
|
|
312
|
+
**Command form:** `msteams <command> [value] [--flag value]`.
|
|
313
|
+
|
|
314
|
+
- **Commands** are the tool names without the `teams_`/`get_` prefix (`search`, `send_message`, `thread`, `meetings`). Any unambiguous abbreviation works, so `send` reaches `send_message`; the full tool name always works too.
|
|
315
|
+
- **Flags** are the tool's own parameters, in camelCase (`--conversationId`, `--threadRootId`, `--fromUrl`). A few short aliases exist where they read better: `--to`, `--msg`, `--reply`, `--user`, `--force`. Booleans work bare (`--markRead`) or with a value (`--markRead false`). Unknown flags are rejected with the list of valid ones.
|
|
316
|
+
- **The first bare value** goes to the command's main parameter: `msteams search "query"` is `--query "query"`.
|
|
317
|
+
- **Output** goes to stdout, diagnostics to stderr, so pipes and redirects behave. Colour and emoji appear only on a terminal, and `NO_COLOR` turns them off.
|
|
318
|
+
- **Exit codes:** `0` success, `1` the tool failed, `2` the command was malformed.
|
|
319
|
+
- **`--json`** prints the tool's JSON payload, ready for `jq`.
|
|
312
320
|
|
|
313
321
|
### From a repo clone
|
|
314
322
|
|
|
315
323
|
If you're working from source, the same CLI is wired to `npm run cli` (runs via `tsx`, no build needed):
|
|
316
324
|
|
|
317
325
|
```bash
|
|
318
|
-
npm run cli # list
|
|
326
|
+
npm run cli # command list
|
|
319
327
|
npm run cli -- search "your query"
|
|
320
328
|
npm run cli -- status
|
|
321
329
|
npm run cli -- send "Hi" --to "conversation-id"
|
|
322
330
|
```
|
|
323
331
|
|
|
324
|
-
|
|
325
|
-
|
|
326
332
|
## Limitations
|
|
327
333
|
|
|
328
334
|
- **Login required** - Run `teams_login` to authenticate (opens browser)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API module exports.
|
|
3
|
+
*/
|
|
4
|
+
export * from './substrate-api.js';
|
|
5
|
+
export * from './chatsvc-api.js';
|
|
6
|
+
export * from './csa-api.js';
|
|
7
|
+
export * from './calendar-api.js';
|
|
8
|
+
export * from './transcript-api.js';
|
|
9
|
+
export * from './files-api.js';
|
|
10
|
+
export * from './tags-api.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API module exports.
|
|
3
|
+
*/
|
|
4
|
+
export * from './substrate-api.js';
|
|
5
|
+
export * from './chatsvc-api.js';
|
|
6
|
+
export * from './csa-api.js';
|
|
7
|
+
export * from './calendar-api.js';
|
|
8
|
+
export * from './transcript-api.js';
|
|
9
|
+
export * from './files-api.js';
|
|
10
|
+
export * from './tags-api.js';
|
package/dist/browser/auth.js
CHANGED
|
@@ -386,7 +386,7 @@ export async function ensureAuthenticated(page, context, onProgress, showOverlay
|
|
|
386
386
|
log('Navigating to Teams...');
|
|
387
387
|
const status = await navigateToTeams(page);
|
|
388
388
|
if (status.isAuthenticated) {
|
|
389
|
-
log('
|
|
389
|
+
log('Teams loaded — saving session state.');
|
|
390
390
|
await saveSessionState(context);
|
|
391
391
|
// Check if the saved tokens are actually valid
|
|
392
392
|
// Browser session cookies can outlive MSAL tokens (cookies last days, tokens ~1 hour)
|
|
@@ -407,9 +407,11 @@ export async function ensureAuthenticated(page, context, onProgress, showOverlay
|
|
|
407
407
|
// Still no valid tokens after waiting
|
|
408
408
|
throw new Error('Headless SSO failed: MSAL token refresh timed out');
|
|
409
409
|
}
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
410
|
+
// No redirect to login does not mean signed in: Teams can render with the
|
|
411
|
+
// Microsoft SSO session gone and only a dead SPA refresh token in its cache.
|
|
412
|
+
// Keep the window open until a live token appears (silently or by sign-in).
|
|
413
|
+
log('Tokens expired. Sign in in the browser window if prompted.');
|
|
414
|
+
await waitForManualLogin(page, context, onProgress, showOverlay);
|
|
413
415
|
return;
|
|
414
416
|
}
|
|
415
417
|
// User interaction required - fail fast if headless
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
vi.mock('./context.js', () => ({ saveSessionState: vi.fn() }));
|
|
3
3
|
vi.mock('../auth/token-extractor.js', () => ({ extractSubstrateToken: vi.fn() }));
|
|
4
|
-
import { getAuthStatus, isTeamsUrl } from './auth.js';
|
|
4
|
+
import { ensureAuthenticated, getAuthStatus, isTeamsUrl } from './auth.js';
|
|
5
5
|
/** A page on `url` with no Teams DOM markers and `tokenMinutes` left on MSAL. */
|
|
6
6
|
function fakePage(url, tokenMinutes) {
|
|
7
7
|
return {
|
|
@@ -43,3 +43,22 @@ describe('getAuthStatus', () => {
|
|
|
43
43
|
expect(status).toMatchObject({ isAuthenticated: false, isOnLoginPage: true });
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
+
describe('ensureAuthenticated', () => {
|
|
47
|
+
it('keeps a visible browser open for sign-in when Teams loads with expired tokens', async () => {
|
|
48
|
+
// Teams can render without redirecting to login while the Microsoft SSO
|
|
49
|
+
// session is gone; only a fresh MSAL token proves the user has signed in.
|
|
50
|
+
const tokenMinutes = [-1, -1, 55];
|
|
51
|
+
const page = {
|
|
52
|
+
goto: async () => null,
|
|
53
|
+
waitForURL: async () => { throw new Error('timeout'); },
|
|
54
|
+
url: () => 'https://teams.cloud.microsoft/',
|
|
55
|
+
evaluate: async () => tokenMinutes.length > 1 ? tokenMinutes.shift() : tokenMinutes[0],
|
|
56
|
+
waitForTimeout: async () => { },
|
|
57
|
+
};
|
|
58
|
+
const messages = [];
|
|
59
|
+
await ensureAuthenticated(page, {}, msg => messages.push(msg), false, false);
|
|
60
|
+
expect(messages).toContain('Waiting for manual login...');
|
|
61
|
+
expect(messages).toContain('Authentication successful!');
|
|
62
|
+
expect(tokenMinutes).toEqual([55]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help text for the `msteams` CLI, generated from the tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* The index stays to one screen: one line per command, first sentence only.
|
|
5
|
+
* Full parameter detail lives in `msteams help <command>`, and the complete
|
|
6
|
+
* schemas in `msteams help --json`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
/**
|
|
10
|
+
* First sentence of a description. The tool descriptions are written for an
|
|
11
|
+
* LLM and run long, so only the opening sentence is useful in a command list.
|
|
12
|
+
* A period followed by a non-capital (as in "e.g.") does not end a sentence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function firstSentence(text: string): string;
|
|
15
|
+
/** The one-screen command index. */
|
|
16
|
+
export declare function renderIndex(tools: Tool[], groups: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
tools: string[];
|
|
19
|
+
}>, version: string): string;
|
|
20
|
+
/** Detail for one command: what it does, how to call it, every flag. */
|
|
21
|
+
export declare function renderToolHelp(tools: Tool[], tool: Tool): string;
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help text for the `msteams` CLI, generated from the tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* The index stays to one screen: one line per command, first sentence only.
|
|
5
|
+
* Full parameter detail lives in `msteams help <command>`, and the complete
|
|
6
|
+
* schemas in `msteams help --json`.
|
|
7
|
+
*/
|
|
8
|
+
import { FLAG_ALIASES, positionalProp, requiredProps, schemaProps, shortName } from './resolve.js';
|
|
9
|
+
import { bold, dim, termWidth, wrap } from './render.js';
|
|
10
|
+
/**
|
|
11
|
+
* First sentence of a description. The tool descriptions are written for an
|
|
12
|
+
* LLM and run long, so only the opening sentence is useful in a command list.
|
|
13
|
+
* A period followed by a non-capital (as in "e.g.") does not end a sentence.
|
|
14
|
+
*/
|
|
15
|
+
export function firstSentence(text) {
|
|
16
|
+
const match = /^(.*?[.!?])(?=\s+[A-Z]|$)/s.exec(text.trim());
|
|
17
|
+
const sentence = (match?.[1] ?? text).replace(/\s+/g, ' ').trim();
|
|
18
|
+
return sentence.replace(/\.$/, '');
|
|
19
|
+
}
|
|
20
|
+
function flagLabel(prop) {
|
|
21
|
+
if (prop.enum)
|
|
22
|
+
return `<${prop.enum.join('|')}>`;
|
|
23
|
+
if (prop.type === 'boolean')
|
|
24
|
+
return '';
|
|
25
|
+
if (prop.type === 'array')
|
|
26
|
+
return '<a,b>';
|
|
27
|
+
return `<${prop.type ?? 'string'}>`;
|
|
28
|
+
}
|
|
29
|
+
/** Aliases pointing at a parameter, e.g. conversationId -> ['--to']. */
|
|
30
|
+
function aliasesFor(prop) {
|
|
31
|
+
return Object.entries(FLAG_ALIASES)
|
|
32
|
+
.filter(([, target]) => target === prop)
|
|
33
|
+
.map(([alias]) => `--${alias}`);
|
|
34
|
+
}
|
|
35
|
+
/** The one-screen command index. */
|
|
36
|
+
export function renderIndex(tools, groups, version) {
|
|
37
|
+
const names = tools.map(tool => tool.name);
|
|
38
|
+
const byName = new Map(tools.map(tool => [tool.name, tool]));
|
|
39
|
+
const labels = new Map(names.map(name => [name, shortName(names, name)]));
|
|
40
|
+
const column = Math.min(22, Math.max(...[...labels.values()].map(label => label.length)) + 2);
|
|
41
|
+
const lines = [
|
|
42
|
+
`${bold('msteams')} ${dim(version)} - Microsoft Teams from your terminal`,
|
|
43
|
+
'',
|
|
44
|
+
bold('Usage'),
|
|
45
|
+
' msteams <command> [value] [--flag value]',
|
|
46
|
+
` msteams help <command> ${dim('flags and detail for one command')}`,
|
|
47
|
+
` msteams <command> --json ${dim('machine-readable output')}`,
|
|
48
|
+
];
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
const members = group.tools.filter(name => byName.has(name));
|
|
51
|
+
if (members.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
lines.push('', bold(group.name));
|
|
54
|
+
for (const name of members) {
|
|
55
|
+
const label = labels.get(name) ?? name;
|
|
56
|
+
const summary = firstSentence(byName.get(name)?.description ?? '');
|
|
57
|
+
const room = termWidth() - column - 4;
|
|
58
|
+
lines.push(` ${label.padEnd(column)}${dim(summary.length > room ? `${summary.slice(0, room - 1)}…` : summary)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push('', dim('Commands are tool names without the teams_/get_ prefix; any unambiguous'), dim("abbreviation works (send -> send_message). 'msteams login' first."));
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
/** Detail for one command: what it does, how to call it, every flag. */
|
|
65
|
+
export function renderToolHelp(tools, tool) {
|
|
66
|
+
const names = tools.map(t => t.name);
|
|
67
|
+
const label = shortName(names, tool.name);
|
|
68
|
+
const props = schemaProps(tool);
|
|
69
|
+
const required = new Set(requiredProps(tool));
|
|
70
|
+
const positional = positionalProp(tool);
|
|
71
|
+
const usage = [` msteams ${label}`];
|
|
72
|
+
if (positional)
|
|
73
|
+
usage[0] += ` <${positional}>`;
|
|
74
|
+
if (Object.keys(props).length > (positional ? 1 : 0))
|
|
75
|
+
usage[0] += ' [--flag value]';
|
|
76
|
+
const lines = [
|
|
77
|
+
`${bold(`msteams ${label}`)} ${dim(`(${tool.name})`)}`,
|
|
78
|
+
'',
|
|
79
|
+
...wrap(firstSentence(tool.description ?? ''), 0, 3).map(line => ` ${line}`),
|
|
80
|
+
'',
|
|
81
|
+
bold('Usage'),
|
|
82
|
+
...usage,
|
|
83
|
+
];
|
|
84
|
+
const entries = Object.entries(props);
|
|
85
|
+
if (entries.length > 0) {
|
|
86
|
+
lines.push('', bold('Flags'));
|
|
87
|
+
const column = Math.min(28, Math.max(...entries.map(([name, prop]) => `--${name} ${flagLabel(prop)}`.trim().length)) + 2);
|
|
88
|
+
for (const [name, prop] of entries) {
|
|
89
|
+
const flag = `--${name} ${flagLabel(prop)}`.trim();
|
|
90
|
+
const alias = aliasesFor(name);
|
|
91
|
+
const detail = [
|
|
92
|
+
required.has(name) ? 'Required.' : '',
|
|
93
|
+
alias.length ? `(or ${alias.join(', ')})` : '',
|
|
94
|
+
prop.description ?? '',
|
|
95
|
+
].filter(Boolean).join(' ');
|
|
96
|
+
const wrapped = wrap(detail, column + 2, 2);
|
|
97
|
+
// A flag too long for the column takes a line of its own.
|
|
98
|
+
if (flag.length >= column) {
|
|
99
|
+
lines.push(` ${bold(flag)}`);
|
|
100
|
+
for (const line of wrapped)
|
|
101
|
+
lines.push(` ${' '.repeat(column)}${line}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lines.push(` ${bold(flag.padEnd(column))}${wrapped[0] ?? ''}`);
|
|
105
|
+
for (const line of wrapped.slice(1))
|
|
106
|
+
lines.push(` ${' '.repeat(column)}${line}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
lines.push('', dim('Add --json for the raw payload.'));
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output for the `msteams` CLI.
|
|
3
|
+
*
|
|
4
|
+
* One generic record renderer covers every tool response: anything with an
|
|
5
|
+
* array gets a numbered list, anything else gets labelled fields. Colour and
|
|
6
|
+
* emoji only appear on a TTY, so piped output stays clean.
|
|
7
|
+
*/
|
|
8
|
+
export declare const bold: (text: string) => string;
|
|
9
|
+
export declare const dim: (text: string) => string;
|
|
10
|
+
export declare const icon: (emoji: string) => string;
|
|
11
|
+
/** Terminal width, with a sane default when output is piped. */
|
|
12
|
+
export declare const termWidth: () => number;
|
|
13
|
+
export declare function oneLine(text: string, max?: number): string;
|
|
14
|
+
/** Renders any field value as a single line, or '' when there is nothing to show. */
|
|
15
|
+
export declare function summarise(value: unknown, max?: number, depth?: number): string;
|
|
16
|
+
/** Prints a successful tool response. */
|
|
17
|
+
export declare function printResponse(response: Record<string, unknown>): void;
|
|
18
|
+
/** Prints a failed tool response to stderr. */
|
|
19
|
+
export declare function printFailure(response: Record<string, unknown>): void;
|
|
20
|
+
/** Wraps text to the terminal width with a hanging indent, clipped to maxLines. */
|
|
21
|
+
export declare function wrap(text: string, indent: number, maxLines?: number): string[];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output for the `msteams` CLI.
|
|
3
|
+
*
|
|
4
|
+
* One generic record renderer covers every tool response: anything with an
|
|
5
|
+
* array gets a numbered list, anything else gets labelled fields. Colour and
|
|
6
|
+
* emoji only appear on a TTY, so piped output stays clean.
|
|
7
|
+
*/
|
|
8
|
+
const COLOUR = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
9
|
+
export const bold = (text) => (COLOUR ? `\x1b[1m${text}\x1b[0m` : text);
|
|
10
|
+
export const dim = (text) => (COLOUR ? `\x1b[2m${text}\x1b[0m` : text);
|
|
11
|
+
export const icon = (emoji) => (COLOUR ? `${emoji} ` : '');
|
|
12
|
+
/** Terminal width, with a sane default when output is piped. */
|
|
13
|
+
export const termWidth = () => process.stdout.columns ?? 80;
|
|
14
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T/;
|
|
15
|
+
/** Fields tried, in order, as an item's headline. */
|
|
16
|
+
const HEADLINE_KEYS = ['content', 'displayName', 'subject', 'name', 'topic', 'title', 'text', 'preview', 'channelName', 'fileName'];
|
|
17
|
+
/** Response keys that are metadata rather than content. */
|
|
18
|
+
const META_KEYS = new Set(['success', 'pagination', 'resultCount', 'count', 'total']);
|
|
19
|
+
export function oneLine(text, max = termWidth() - 4) {
|
|
20
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
22
|
+
}
|
|
23
|
+
/** Best human label for an object value (sender shapes, profiles, mentions). */
|
|
24
|
+
function objectLabel(value) {
|
|
25
|
+
const email = value.EmailAddress;
|
|
26
|
+
const candidate = value.displayName ?? value.name ?? value.Name ?? email?.Name ?? email?.Address;
|
|
27
|
+
return candidate === undefined || candidate === null ? null : String(candidate);
|
|
28
|
+
}
|
|
29
|
+
/** Renders any field value as a single line, or '' when there is nothing to show. */
|
|
30
|
+
export function summarise(value, max = termWidth() - 4, depth = 0) {
|
|
31
|
+
if (value === null || value === undefined || value === '')
|
|
32
|
+
return '';
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
if (value.length === 0)
|
|
35
|
+
return '';
|
|
36
|
+
const scalars = value.filter(item => typeof item !== 'object' || item === null);
|
|
37
|
+
return scalars.length === value.length
|
|
38
|
+
? oneLine(value.join(', '), max)
|
|
39
|
+
: `${value.length} items`;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'object') {
|
|
42
|
+
const label = objectLabel(value);
|
|
43
|
+
if (label !== null)
|
|
44
|
+
return label;
|
|
45
|
+
if (depth > 0)
|
|
46
|
+
return oneLine(JSON.stringify(value), max);
|
|
47
|
+
// No name to show, so inline the fields: `available=true, expiresAt=...`.
|
|
48
|
+
const parts = Object.entries(value)
|
|
49
|
+
.map(([key, nested]) => [key, summarise(nested, max, depth + 1)])
|
|
50
|
+
.filter(([, text]) => text !== '')
|
|
51
|
+
.map(([key, text]) => `${key}=${text}`);
|
|
52
|
+
return parts.length ? oneLine(parts.join(', '), max) : '';
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'string' && ISO_DATE.test(value)) {
|
|
55
|
+
const date = new Date(value);
|
|
56
|
+
if (!Number.isNaN(date.getTime()))
|
|
57
|
+
return date.toLocaleString();
|
|
58
|
+
}
|
|
59
|
+
return oneLine(String(value), max);
|
|
60
|
+
}
|
|
61
|
+
/** A nested object with more fields than fit on one line, e.g. a profile. */
|
|
62
|
+
function isBlock(value) {
|
|
63
|
+
return typeof value === 'object'
|
|
64
|
+
&& value !== null
|
|
65
|
+
&& !Array.isArray(value)
|
|
66
|
+
&& Object.keys(value).length > 3;
|
|
67
|
+
}
|
|
68
|
+
function printFields(record, indent, skip = new Set()) {
|
|
69
|
+
for (const [key, value] of Object.entries(record)) {
|
|
70
|
+
if (skip.has(key) || META_KEYS.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
if (indent === '' && isBlock(value)) {
|
|
73
|
+
console.log(dim(`${key}:`));
|
|
74
|
+
printFields(value, ' ');
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const text = summarise(value, termWidth() - indent.length - key.length - 3);
|
|
78
|
+
if (text)
|
|
79
|
+
console.log(`${indent}${dim(`${key}:`)} ${text}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function printItem(item, number) {
|
|
83
|
+
if (typeof item !== 'object' || item === null) {
|
|
84
|
+
console.log(`${bold(`${number}.`)} ${summarise(item)}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const record = item;
|
|
88
|
+
const headlineKey = HEADLINE_KEYS.find(key => typeof record[key] === 'string' && record[key] !== '');
|
|
89
|
+
const headline = headlineKey ? oneLine(String(record[headlineKey])) : '';
|
|
90
|
+
console.log(`${bold(`${number}.`)} ${headline || dim('—')}`);
|
|
91
|
+
printFields(record, ' ', new Set(headlineKey ? [headlineKey] : []));
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
/** Prints a successful tool response. */
|
|
95
|
+
export function printResponse(response) {
|
|
96
|
+
const listEntry = Object.entries(response)
|
|
97
|
+
.find(([key, value]) => Array.isArray(value) && !META_KEYS.has(key));
|
|
98
|
+
if (listEntry) {
|
|
99
|
+
const [key, value] = listEntry;
|
|
100
|
+
const pagination = response.pagination;
|
|
101
|
+
const offset = typeof pagination?.from === 'number' ? pagination.from : 0;
|
|
102
|
+
const count = Number(response.resultCount ?? value.length);
|
|
103
|
+
const label = count === 1 ? key.replace(/s$/, '') : key;
|
|
104
|
+
console.log(`${bold(String(count))} ${label}:\n`);
|
|
105
|
+
value.forEach((item, index) => printItem(item, offset + index + 1));
|
|
106
|
+
printFields(response, '', new Set([key]));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
printFields(response, '');
|
|
110
|
+
}
|
|
111
|
+
const pagination = response.pagination;
|
|
112
|
+
if (pagination) {
|
|
113
|
+
const total = pagination.total !== undefined ? ` of ${pagination.total}` : '';
|
|
114
|
+
console.log(dim(`\nShowing ${pagination.returned}${total} (from ${pagination.from}, size ${pagination.size})`));
|
|
115
|
+
if (pagination.hasMore)
|
|
116
|
+
console.log(dim(`More available: --from ${pagination.nextFrom}`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Prints a failed tool response to stderr. */
|
|
120
|
+
export function printFailure(response) {
|
|
121
|
+
console.error(`${icon('❌')}${response.error ?? 'Unknown error'}`);
|
|
122
|
+
if (response.errorCode)
|
|
123
|
+
console.error(dim(` code: ${response.errorCode}`));
|
|
124
|
+
const suggestions = response.suggestions;
|
|
125
|
+
if (Array.isArray(suggestions)) {
|
|
126
|
+
for (const suggestion of suggestions)
|
|
127
|
+
console.error(dim(` try: ${suggestion}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Wraps text to the terminal width with a hanging indent, clipped to maxLines. */
|
|
131
|
+
export function wrap(text, indent, maxLines = 2) {
|
|
132
|
+
const width = Math.max(40, termWidth()) - indent;
|
|
133
|
+
const lines = [];
|
|
134
|
+
let line = '';
|
|
135
|
+
for (const word of text.replace(/\s+/g, ' ').trim().split(' ')) {
|
|
136
|
+
const next = line ? `${line} ${word}` : word;
|
|
137
|
+
if (next.length <= width) {
|
|
138
|
+
line = next;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (line)
|
|
142
|
+
lines.push(line);
|
|
143
|
+
line = word;
|
|
144
|
+
}
|
|
145
|
+
if (lines.length > maxLines)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (line)
|
|
149
|
+
lines.push(line);
|
|
150
|
+
if (lines.length <= maxLines)
|
|
151
|
+
return lines;
|
|
152
|
+
const clipped = lines.slice(0, maxLines);
|
|
153
|
+
clipped[maxLines - 1] = `${clipped[maxLines - 1].slice(0, Math.max(0, width - 2))}…`;
|
|
154
|
+
return clipped;
|
|
155
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command and argument resolution for the `msteams` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is derived from the MCP tool definitions - their names and
|
|
5
|
+
* JSON input schemas - so a new tool is callable, documented and type-coerced
|
|
6
|
+
* without touching the CLI.
|
|
7
|
+
*/
|
|
8
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
/** Thrown for anything the user can fix by retyping the command (exit code 2). */
|
|
10
|
+
export declare class UsageError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/** The subset of JSON Schema the tool definitions use. */
|
|
13
|
+
export interface SchemaProp {
|
|
14
|
+
type?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
enum?: string[];
|
|
17
|
+
items?: {
|
|
18
|
+
type?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Flag aliases that read better than the raw parameter name. */
|
|
22
|
+
export declare const FLAG_ALIASES: Record<string, string>;
|
|
23
|
+
export declare function schemaProps(tool: Tool): Record<string, SchemaProp>;
|
|
24
|
+
export declare function requiredProps(tool: Tool): string[];
|
|
25
|
+
/** The parameter a bare value after the command maps to: the first required string. */
|
|
26
|
+
export declare function positionalProp(tool: Tool): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves a typed command to a tool name.
|
|
29
|
+
*
|
|
30
|
+
* Tries, in order: the exact name, `teams_<cmd>`, `teams_get_<cmd>`,
|
|
31
|
+
* `teams_search_<cmd>`, a unique `teams_<cmd>*` prefix, then a unique
|
|
32
|
+
* substring match ignoring underscores. So `search`, `get_thread`, `thread`,
|
|
33
|
+
* `send` and `markread` all land on the right tool.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveToolName(names: string[], command: string): string;
|
|
36
|
+
/** The shortest form that still resolves back to this tool - used in help output. */
|
|
37
|
+
export declare function shortName(names: string[], toolName: string): string;
|
|
38
|
+
export interface ParsedToolArgs {
|
|
39
|
+
args: Record<string, unknown>;
|
|
40
|
+
json: boolean;
|
|
41
|
+
help: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parses a command's arguments against its input schema.
|
|
45
|
+
*
|
|
46
|
+
* Flags are whatever the schema declares (plus the aliases above), values are
|
|
47
|
+
* coerced to the schema's types, and an unknown flag is a usage error rather
|
|
48
|
+
* than something the server has to reject.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseToolArgs(tool: Tool, argv: string[], label?: string): ParsedToolArgs;
|