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 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. Run with no arguments to list all tools and shortcuts.
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
- # List available tools and shortcuts
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 teams_search_email --query "from:sarah@company.com"
296
+ msteams email "from:sarah@company.com"
292
297
 
293
- # Send a message (default: your own notes/self-chat)
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
- # Any tool by name (the teams_ prefix is optional)
304
- msteams teams_search_emoji --query "heart"
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> [primaryArg] [--key value ...]`. Any unrecognised command is treated as a tool name (`teams_` is added automatically). Common flags like `--to`, `--from`, `--size`, `--query`, `--force` map to the matching tool parameters; run `msteams` with no arguments to see the full list.
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 tools
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';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Auth module exports.
3
+ */
4
+ export * from './session-store.js';
5
+ export * from './token-extractor.js';
6
+ export * from './token-refresh.js';
7
+ export { encrypt, decrypt, isEncrypted, type EncryptedData } from './crypto.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Auth module exports.
3
+ */
4
+ export * from './session-store.js';
5
+ export * from './token-extractor.js';
6
+ export * from './token-refresh.js';
7
+ export { encrypt, decrypt, isEncrypted } from './crypto.js';
@@ -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('Already authenticated — saving session state.');
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
- // In visible mode, the tokens might refresh while user interacts, or they can
411
- // manually complete any prompts. We've saved what we have.
412
- log('Session state saved (tokens may need refresh).');
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;
@@ -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;