@rockhopper-co/mcp-server 2.0.1 → 2.1.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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/dist/api-client.d.ts +136 -2
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +185 -2
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/capabilities.d.ts +5 -6
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +6 -9
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/drive-search.d.ts +162 -0
  11. package/dist/drive-search.d.ts.map +1 -0
  12. package/dist/drive-search.js +210 -0
  13. package/dist/drive-search.js.map +1 -0
  14. package/dist/enrollment.d.ts +121 -0
  15. package/dist/enrollment.d.ts.map +1 -0
  16. package/dist/enrollment.js +247 -0
  17. package/dist/enrollment.js.map +1 -0
  18. package/dist/resources/orchestration-guide.md +58 -8
  19. package/dist/tools/connect-microsoft.d.ts +21 -0
  20. package/dist/tools/connect-microsoft.d.ts.map +1 -0
  21. package/dist/tools/connect-microsoft.js +138 -0
  22. package/dist/tools/connect-microsoft.js.map +1 -0
  23. package/dist/tools/drive-search-lanes.d.ts +65 -0
  24. package/dist/tools/drive-search-lanes.d.ts.map +1 -0
  25. package/dist/tools/drive-search-lanes.js +95 -0
  26. package/dist/tools/drive-search-lanes.js.map +1 -0
  27. package/dist/tools/drive-search.contract.d.ts +94 -0
  28. package/dist/tools/drive-search.contract.d.ts.map +1 -0
  29. package/dist/tools/drive-search.contract.js +235 -0
  30. package/dist/tools/drive-search.contract.js.map +1 -0
  31. package/dist/tools/drive-search.d.ts +31 -0
  32. package/dist/tools/drive-search.d.ts.map +1 -0
  33. package/dist/tools/drive-search.js +209 -0
  34. package/dist/tools/drive-search.js.map +1 -0
  35. package/dist/tools/enroll-file.contract.d.ts +52 -0
  36. package/dist/tools/enroll-file.contract.d.ts.map +1 -0
  37. package/dist/tools/enroll-file.contract.js +105 -0
  38. package/dist/tools/enroll-file.contract.js.map +1 -0
  39. package/dist/tools/enroll-file.d.ts +4 -0
  40. package/dist/tools/enroll-file.d.ts.map +1 -0
  41. package/dist/tools/enroll-file.js +184 -0
  42. package/dist/tools/enroll-file.js.map +1 -0
  43. package/dist/tools/index.d.ts.map +1 -1
  44. package/dist/tools/index.js +12 -0
  45. package/dist/tools/index.js.map +1 -1
  46. package/dist/tools/search.d.ts.map +1 -1
  47. package/dist/tools/search.js +25 -2
  48. package/dist/tools/search.js.map +1 -1
  49. package/dist/tools/write-files.d.ts.map +1 -1
  50. package/dist/tools/write-files.js +5 -0
  51. package/dist/tools/write-files.js.map +1 -1
  52. package/dist/types.d.ts +150 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +2 -2
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ENG-2204 — how the confirmation question reaches a human, given a client
3
+ * that may support one, two or three ways of asking one.
4
+ *
5
+ * There are three lanes and they are not alternatives to pick between — they
6
+ * are a ladder, and every client can climb at least the bottom rung:
7
+ *
8
+ * 1. **The tool result** (universal). The answer says "ask the user which one,
9
+ * then call again with their number". Works on every client that has ever
10
+ * spoken MCP, because it is just text.
11
+ * 2. **`elicitation/create`** (2025-era sessions that advertise it). The client
12
+ * renders a real picker; the server waits for the answer inside the call.
13
+ * 3. **`InputRequiredResult`** (2026-07-28 sessions). Elicitation is gone from
14
+ * that revision as a server→client request; the same question rides back as
15
+ * a result the client fulfils and retries.
16
+ *
17
+ * The ladder matters more than any one rung. A client that supports none of
18
+ * the richer lanes still gets a usable question — it does NOT get an enrolment
19
+ * it never confirmed, and it does not get an error. Degrading is the design.
20
+ */
21
+ import { CLIENT_CAPABILITIES_META_KEY, PROTOCOL_VERSION_META_KEY, } from '@modelcontextprotocol/server';
22
+ /** The protocol revision on which lane 3 is the only richer lane there is. */
23
+ const MRTR_PROTOCOL_VERSION = '2026-07-28';
24
+ /** The key both richer lanes carry the confirmation under. */
25
+ export const CONFIRM_KEY = 'confirm_file';
26
+ /** Read one key off the per-request envelope, which is typed as an open bag. */
27
+ function envelopeValue(ctx, key) {
28
+ const envelope = ctx?.mcpReq?.envelope;
29
+ return envelope?.[key];
30
+ }
31
+ /**
32
+ * Whether this REQUEST is on the 2026-07-28 wire.
33
+ *
34
+ * Read off the request envelope rather than off the connection, because that
35
+ * is where the SDK puts it: `_meta['io.modelcontextprotocol/protocolVersion']`
36
+ * arrives on every modern request, and `stdio-2026-07-28.e2e.test.ts` drives
37
+ * exactly that shape.
38
+ */
39
+ export function isModernEra(ctx) {
40
+ return envelopeValue(ctx, PROTOCOL_VERSION_META_KEY) === MRTR_PROTOCOL_VERSION;
41
+ }
42
+ /**
43
+ * Whether the client said it can render an elicitation.
44
+ *
45
+ * Two sources, checked in this order and for different reasons. A modern
46
+ * request carries the client's capabilities in its own envelope, which is the
47
+ * authority for THAT request. A 2025-era session declared them once at
48
+ * `initialize`, and the connection is the only place that survives.
49
+ *
50
+ * ABSENT means NO. A client that never said it can ask a question is not
51
+ * assumed to be able to — the cost of guessing wrong is a call that hangs
52
+ * waiting for an answer nobody will ever be shown.
53
+ */
54
+ export function advertisesElicitation(ctx, connectionCapabilities) {
55
+ const fromEnvelope = envelopeValue(ctx, CLIENT_CAPABILITIES_META_KEY);
56
+ if (fromEnvelope && typeof fromEnvelope === 'object') {
57
+ return 'elicitation' in fromEnvelope;
58
+ }
59
+ return !!connectionCapabilities && 'elicitation' in connectionCapabilities;
60
+ }
61
+ /**
62
+ * The best rung this request can climb.
63
+ *
64
+ * Lane 2 is deliberately unreachable on the modern era even when the client
65
+ * advertises `elicitation`: the SDK's `ctx.mcpReq.elicitInput` THROWS on a
66
+ * 2026-07-28 request, so treating the capability as sufficient would turn the
67
+ * richest client into the only one that fails.
68
+ */
69
+ export function selectLane(ctx, connectionCapabilities) {
70
+ if (isModernEra(ctx))
71
+ return 'input_required';
72
+ if (advertisesElicitation(ctx, connectionCapabilities))
73
+ return 'elicitation';
74
+ return 'tool_result';
75
+ }
76
+ /**
77
+ * The nonce a retried modern request is echoing back.
78
+ *
79
+ * Untrusted by construction — it round-trips through the client, nothing signs
80
+ * it, and the SDK hands back whatever arrived. It is used as a Map key and for
81
+ * nothing else: an unrecognised one finds no candidate set, which is the same
82
+ * answer as a malicious one.
83
+ */
84
+ export function readRequestState(ctx) {
85
+ try {
86
+ const state = ctx?.mcpReq?.requestState?.();
87
+ return typeof state === 'string' ? state : null;
88
+ }
89
+ catch {
90
+ // A configured verify hook rejecting the state is a refusal, not a crash:
91
+ // the caller treats "no usable nonce" and "a nonce we do not know" alike.
92
+ return null;
93
+ }
94
+ }
95
+ //# sourceMappingURL=drive-search-lanes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive-search-lanes.js","sourceRoot":"","sources":["../../src/tools/drive-search-lanes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,GAE1B,MAAM,8BAA8B,CAAC;AAEtC,8EAA8E;AAC9E,MAAM,qBAAqB,GAAG,YAAY,CAAC;AAE3C,8DAA8D;AAC9D,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAI1C,gFAAgF;AAChF,SAAS,aAAa,CAAC,GAA8B,EAAE,GAAW;IAChE,MAAM,QAAQ,GAAG,GAAG,EAAE,MAAM,EAAE,QAA+C,CAAC;IAC9E,OAAO,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAA8B;IACxD,OAAO,aAAa,CAAC,GAAG,EAAE,yBAAyB,CAAC,KAAK,qBAAqB,CAAC;AACjF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAA8B,EAC9B,sBAA2D;IAE3D,MAAM,YAAY,GAAG,aAAa,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAC;IACtE,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;QACrD,OAAO,aAAa,IAAI,YAAY,CAAC;IACvC,CAAC;IACD,OAAO,CAAC,CAAC,sBAAsB,IAAI,aAAa,IAAI,sBAAsB,CAAC;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CACxB,GAA8B,EAC9B,sBAA2D;IAE3D,IAAI,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAC9C,IAAI,qBAAqB,CAAC,GAAG,EAAE,sBAAsB,CAAC;QAAE,OAAO,aAAa,CAAC;IAC7E,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAA8B;IAC7D,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC;QAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,0EAA0E;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ENG-2204 — everything `search_drive_files` SAYS, split from everything it
3
+ * DOES (`drive-search.ts`), on the seam ENG-2200 cut for `enroll_file`.
4
+ *
5
+ * This half is the only interface a language model ever sees: it cannot read
6
+ * the handler, so the tool description and the schema descriptions ARE the
7
+ * contract, and a wrong word here is a behaviour bug no type checks. Keeping
8
+ * it apart means a person can review it as prose — deciding whether a model
9
+ * would do the right thing — without the lane branching in the way.
10
+ */
11
+ import type { ElicitRequestFormParams } from '@modelcontextprotocol/server';
12
+ import { z } from 'zod';
13
+ import { type Candidate, type DriveSearchOutcome } from '../drive-search.js';
14
+ export declare const DRIVE_SEARCH_INPUT_SCHEMA: z.ZodObject<{
15
+ query: z.ZodOptional<z.ZodString>;
16
+ scope: z.ZodOptional<z.ZodEnum<{
17
+ search: "search";
18
+ recent: "recent";
19
+ }>>;
20
+ limit: z.ZodOptional<z.ZodNumber>;
21
+ confirm_index: z.ZodOptional<z.ZodNumber>;
22
+ confirm_token: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>;
24
+ export declare const DRIVE_SEARCH_DESCRIPTION: string;
25
+ export declare const DRIVE_SEARCH_ANNOTATIONS: {
26
+ readonly readOnlyHint: true;
27
+ readonly openWorldHint: true;
28
+ };
29
+ /** The machine-readable half of every answer, so a model can branch on it. */
30
+ export interface DriveSearchAnswer {
31
+ outcome: DriveSearchOutcome;
32
+ text: string;
33
+ isError?: boolean;
34
+ detail?: Record<string, unknown>;
35
+ }
36
+ /**
37
+ * Render an answer as a tool result: prose for the human, then one JSON line
38
+ * for the model. Both, because prose alone invites the model to paraphrase a
39
+ * refusal into a success, and JSON alone gives the user nothing to read.
40
+ * Same shape as `enroll_file`'s, so the two tools read alike in a transcript.
41
+ */
42
+ export declare function toolResult(answer: DriveSearchAnswer): {
43
+ isError?: true | undefined;
44
+ content: {
45
+ type: "text";
46
+ text: string;
47
+ }[];
48
+ };
49
+ /** How one candidate reads to a user, without its identifiers. */
50
+ export declare function candidateLabel(candidate: Candidate): string;
51
+ /** The numbered list the user picks from, and the model quotes back. */
52
+ export declare function renderCandidates(candidates: readonly Candidate[]): string;
53
+ /** The question the model puts to the user when no richer lane exists. */
54
+ export declare function confirmationPrompt(candidates: readonly Candidate[], token: string): string;
55
+ /** The elicitation form both richer lanes send. */
56
+ export declare function confirmationForm(candidates: readonly Candidate[]): Omit<ElicitRequestFormParams, 'mode'>;
57
+ /** The answer that means "none of these" rather than a position. */
58
+ export declare const DECLINE_CHOICE = "none";
59
+ /**
60
+ * The answer once a specific candidate has been confirmed by a human.
61
+ *
62
+ * It hands over identifiers and stops. Enrolling here would mean a second copy
63
+ * of `enroll_file`'s logic — the share_with question, the hidden-file restore
64
+ * — living in a read tool, and two copies of a rule is how the two answers
65
+ * start to differ.
66
+ */
67
+ export declare function confirmedAnswer(candidate: Candidate): {
68
+ isError?: true | undefined;
69
+ content: {
70
+ type: "text";
71
+ text: string;
72
+ }[];
73
+ };
74
+ /** The answer when the user was asked and said none of these. */
75
+ export declare function declinedAnswer(): {
76
+ isError?: true | undefined;
77
+ content: {
78
+ type: "text";
79
+ text: string;
80
+ }[];
81
+ };
82
+ /** The answer when a confirmation names something this session never offered. */
83
+ export declare function unknownCandidateAnswer(): {
84
+ isError?: true | undefined;
85
+ content: {
86
+ type: "text";
87
+ text: string;
88
+ }[];
89
+ };
90
+ /** Told to the model when the session has searched as much as it may. */
91
+ export declare const BUDGET_EXHAUSTED_TEXT: string;
92
+ /** Told to the model when the user has no delegated Microsoft grant. */
93
+ export declare function connectPrompt(authorizeUrl: string, expiresAt: string): string;
94
+ //# sourceMappingURL=drive-search.contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive-search.contract.d.ts","sourceRoot":"","sources":["../../src/tools/drive-search.contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,yBAAyB;;;;;;;;;iBAsDpC,CAAC;AAEH,eAAO,MAAM,wBAAwB,QAeQ,CAAC;AAE9C,eAAO,MAAM,wBAAwB;;;CAI3B,CAAC;AAEX,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB;;;;;;EAWnD;AAED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAQ3D;AAgBD,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAIzE;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,SAAS,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,MAAM,CAUR;AAED,mDAAmD;AACnD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,SAAS,EAAE,GAC/B,IAAI,CAAC,uBAAuB,EAAE,MAAM,CAAC,CA4BvC;AAED,oEAAoE;AACpE,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS;;;;;;EA0BnD;AAED,iEAAiE;AACjE,wBAAgB,cAAc;;;;;;EAQ7B;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB;;;;;;EASrC;AAED,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,QAIqC,CAAC;AAExE,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAS7E"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * ENG-2204 — everything `search_drive_files` SAYS, split from everything it
3
+ * DOES (`drive-search.ts`), on the seam ENG-2200 cut for `enroll_file`.
4
+ *
5
+ * This half is the only interface a language model ever sees: it cannot read
6
+ * the handler, so the tool description and the schema descriptions ARE the
7
+ * contract, and a wrong word here is a behaviour bug no type checks. Keeping
8
+ * it apart means a person can review it as prose — deciding whether a model
9
+ * would do the right thing — without the lane branching in the way.
10
+ */
11
+ import { z } from 'zod';
12
+ import { DRIVE_SEARCH_SESSION_BUDGET, } from '../drive-search.js';
13
+ export const DRIVE_SEARCH_INPUT_SCHEMA = z.object({
14
+ query: z
15
+ .string()
16
+ .optional()
17
+ .describe('Part of the file name to look for, in the user\'s own words — "Q3 ' +
18
+ 'forecast", "Becklar model". Required unless `scope` is "recent".'),
19
+ scope: z
20
+ .enum(['search', 'recent'])
21
+ .optional()
22
+ .describe('"search" (default) looks across everything the user can reach in ' +
23
+ 'OneDrive and SharePoint. "recent" lists the workbooks they have ' +
24
+ 'worked on lately and needs no `query` — use it when the user cannot ' +
25
+ 'remember the name.'),
26
+ limit: z
27
+ .number()
28
+ .int()
29
+ .min(1)
30
+ .max(50)
31
+ .optional()
32
+ .describe('How many candidates to return. Default 10, maximum 50.'),
33
+ /**
34
+ * The confirmation lane every client has. `confirm_index` is how a client
35
+ * with no elicitation and no multi-round-trip support closes the loop: the
36
+ * model puts the question to the user in the conversation, and calls again
37
+ * carrying the number the user chose.
38
+ *
39
+ * It is an INDEX into a set this session offered, not a file identifier, and
40
+ * that is the whole point. A model that has read a malicious file name
41
+ * cannot invent a `driveMsId` here and have the tool bless it: the index is
42
+ * resolved against server memory, and an index into a set that was never
43
+ * offered resolves to nothing.
44
+ */
45
+ confirm_index: z
46
+ .number()
47
+ .int()
48
+ .optional()
49
+ .describe('The number of the candidate the USER picked from a previous ' +
50
+ '`search_drive_files` answer, after you asked them which file they ' +
51
+ 'meant. Send it together with `confirm_token` from that same answer. ' +
52
+ 'Send 0 if they said none of them is the right file. ' +
53
+ 'Never guess it and never pick on the user\'s behalf.'),
54
+ confirm_token: z
55
+ .string()
56
+ .optional()
57
+ .describe('The `confirm_token` from the answer whose list the user picked from. ' +
58
+ 'Pass it back exactly as it was given.'),
59
+ });
60
+ export const DRIVE_SEARCH_DESCRIPTION = "Find a Microsoft Excel workbook in the user's own OneDrive or SharePoint — " +
61
+ 'including files Rockhopper has never seen. Each candidate comes back marked ' +
62
+ 'as already in Rockhopper or not. ' +
63
+ 'USE THIS when the user names a workbook that `search_files` and ' +
64
+ '`list_files` cannot find: those two see only files already added to ' +
65
+ 'Rockhopper, so "no match" there means "never added", not "no such file". ' +
66
+ 'YOU MUST CONFIRM THE FILE WITH THE USER before enrolling it. Show them the ' +
67
+ 'candidates, ask which one they mean, then call this tool again with their ' +
68
+ 'pick as `confirm_index` plus the `confirm_token`; the answer carries the ' +
69
+ 'identifiers to hand to `enroll_file`. Never enroll a file the user has not ' +
70
+ 'named, and never claim a file is already in Rockhopper because one search ' +
71
+ 'result looked similar. ' +
72
+ 'Searching is capped per session, so search deliberately rather than ' +
73
+ 'browsing. Requires a connected Microsoft account — if none is connected, ' +
74
+ 'this returns the link the user must open.';
75
+ export const DRIVE_SEARCH_ANNOTATIONS = {
76
+ readOnlyHint: true,
77
+ // It reaches SharePoint / OneDrive through Rockhopper.
78
+ openWorldHint: true,
79
+ };
80
+ /**
81
+ * Render an answer as a tool result: prose for the human, then one JSON line
82
+ * for the model. Both, because prose alone invites the model to paraphrase a
83
+ * refusal into a success, and JSON alone gives the user nothing to read.
84
+ * Same shape as `enroll_file`'s, so the two tools read alike in a transcript.
85
+ */
86
+ export function toolResult(answer) {
87
+ const payload = { outcome: answer.outcome, ...(answer.detail ?? {}) };
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: `${answer.text}\n${JSON.stringify(payload)}`,
93
+ },
94
+ ],
95
+ ...(answer.isError ? { isError: true } : {}),
96
+ };
97
+ }
98
+ /** How one candidate reads to a user, without its identifiers. */
99
+ export function candidateLabel(candidate) {
100
+ const parts = [candidate.name];
101
+ if (candidate.parentPath)
102
+ parts.push(candidate.parentPath);
103
+ if (candidate.lastModifiedAt) {
104
+ parts.push(`modified ${candidate.lastModifiedAt}`);
105
+ }
106
+ parts.push(ENROLLMENT_NOTE[candidate.enrollmentState]);
107
+ return parts.join(' · ');
108
+ }
109
+ /**
110
+ * What each enrolment state MEANS to the person choosing, in their words.
111
+ *
112
+ * `hidden` is spelled out rather than folded into "already added" because the
113
+ * two lead to different conversations: one is done, and the other needs the
114
+ * user to say whether they want a file they removed put back. Collapsing them
115
+ * is the ENG-1647 answer — "already enrolled" about a file the user cannot see.
116
+ */
117
+ const ENROLLMENT_NOTE = {
118
+ enrolled: 'already in Rockhopper',
119
+ hidden: 'previously removed from Rockhopper',
120
+ not_enrolled: 'not in Rockhopper yet',
121
+ };
122
+ /** The numbered list the user picks from, and the model quotes back. */
123
+ export function renderCandidates(candidates) {
124
+ return candidates
125
+ .map((c, index) => `${index + 1}. **${c.name}** — ${candidateLabel(c)}`)
126
+ .join('\n');
127
+ }
128
+ /** The question the model puts to the user when no richer lane exists. */
129
+ export function confirmationPrompt(candidates, token) {
130
+ return (`Found ${candidates.length} possible file(s). ` +
131
+ 'DO NOT enroll any of these yet.\n\n' +
132
+ `${renderCandidates(candidates)}\n\n` +
133
+ 'Ask the user which one they meant — quote the names, do not choose for ' +
134
+ 'them. When they answer, call `search_drive_files` again with ' +
135
+ `confirm_index: <their number> and confirm_token: "${token}", or ` +
136
+ 'confirm_index: 0 if none of these is the file.');
137
+ }
138
+ /** The elicitation form both richer lanes send. */
139
+ export function confirmationForm(candidates) {
140
+ return {
141
+ message: 'Which workbook did you mean? Rockhopper will not add anything until ' +
142
+ 'you pick one.',
143
+ requestedSchema: {
144
+ type: 'object',
145
+ properties: {
146
+ // The values are POSITIONS, never identifiers. A client (or anything
147
+ // steering it) can only send back a number, and a number that does not
148
+ // index a set this session offered resolves to nothing.
149
+ choice: {
150
+ type: 'string',
151
+ enum: [
152
+ ...candidates.map((_, index) => String(index + 1)),
153
+ DECLINE_CHOICE,
154
+ ],
155
+ enumNames: [
156
+ ...candidates.map((c) => candidateLabel(c)),
157
+ 'None of these',
158
+ ],
159
+ title: 'File',
160
+ description: 'The workbook to add to Rockhopper.',
161
+ },
162
+ },
163
+ required: ['choice'],
164
+ },
165
+ };
166
+ }
167
+ /** The answer that means "none of these" rather than a position. */
168
+ export const DECLINE_CHOICE = 'none';
169
+ /**
170
+ * The answer once a specific candidate has been confirmed by a human.
171
+ *
172
+ * It hands over identifiers and stops. Enrolling here would mean a second copy
173
+ * of `enroll_file`'s logic — the share_with question, the hidden-file restore
174
+ * — living in a read tool, and two copies of a rule is how the two answers
175
+ * start to differ.
176
+ */
177
+ export function confirmedAnswer(candidate) {
178
+ if (!candidate.driveMsId) {
179
+ return toolResult({
180
+ outcome: 'unknown_candidate',
181
+ isError: true,
182
+ text: `Microsoft did not say which drive "${candidate.name}" lives in, so ` +
183
+ 'it cannot be added from the search result. Ask the user to open the ' +
184
+ 'workbook and paste the address from their browser bar, then call ' +
185
+ '`enroll_file` with that `url`.',
186
+ });
187
+ }
188
+ return toolResult({
189
+ outcome: 'confirmed',
190
+ text: `The user confirmed "${candidate.name}" (${candidateLabel(candidate)}). ` +
191
+ 'Now call `enroll_file` with the `driveMsId` and `msId` below. ' +
192
+ '`enroll_file` will ask who may see the file — put that question to the ' +
193
+ 'user too, and never answer it yourself.',
194
+ detail: {
195
+ name: candidate.name,
196
+ driveMsId: candidate.driveMsId,
197
+ msId: candidate.msId,
198
+ enrollmentState: candidate.enrollmentState,
199
+ },
200
+ });
201
+ }
202
+ /** The answer when the user was asked and said none of these. */
203
+ export function declinedAnswer() {
204
+ return toolResult({
205
+ outcome: 'declined',
206
+ text: 'The user did not pick any of those files, so nothing was added. Ask ' +
207
+ 'them to describe the workbook differently and search again, or to ' +
208
+ 'paste its SharePoint or OneDrive link for `enroll_file`.',
209
+ });
210
+ }
211
+ /** The answer when a confirmation names something this session never offered. */
212
+ export function unknownCandidateAnswer() {
213
+ return toolResult({
214
+ outcome: 'unknown_candidate',
215
+ isError: true,
216
+ text: 'That pick does not match any file this search offered, so nothing was ' +
217
+ 'added. Run `search_drive_files` again and confirm against the list it ' +
218
+ 'returns. Never enroll a file that did not come back from a search.',
219
+ });
220
+ }
221
+ /** Told to the model when the session has searched as much as it may. */
222
+ export const BUDGET_EXHAUSTED_TEXT = `This session has used all ${DRIVE_SEARCH_SESSION_BUDGET} of its file ` +
223
+ 'searches. This is a fixed limit and waiting does not restore it. Stop ' +
224
+ "searching and ask the user to paste the workbook's SharePoint or OneDrive " +
225
+ 'link, then call `enroll_file` with that link. Nothing was searched.';
226
+ /** Told to the model when the user has no delegated Microsoft grant. */
227
+ export function connectPrompt(authorizeUrl, expiresAt) {
228
+ return ('Rockhopper cannot look at this user\'s Microsoft files until they ' +
229
+ 'connect their Microsoft account. Give them this link to open ' +
230
+ `themselves — it expires at ${expiresAt}:\n\n${authorizeUrl}\n\n` +
231
+ 'Rockhopper asks only to READ their files. Once they have approved, call ' +
232
+ '`microsoft_link_status` to confirm, then search again. Do not compose a ' +
233
+ 'sign-in link yourself; this one is the only valid one.');
234
+ }
235
+ //# sourceMappingURL=drive-search.contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive-search.contract.js","sourceRoot":"","sources":["../../src/tools/drive-search.contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type InputRequiredResult, type McpServer } from '@modelcontextprotocol/server';
2
+ import type { ApiClient } from '../api-client.js';
3
+ import { CandidateRegistry, SearchBudget } from '../drive-search.js';
4
+ /**
5
+ * ENG-2204 (SP08) — the half of ENG-1647 that ENG-2200 did not fix.
6
+ *
7
+ * ENG-2200 built the enroll tool the customer could not find. This finds the
8
+ * FILE: a workbook Rockhopper has never seen, in the user's own OneDrive or
9
+ * SharePoint, confirmed with them before anything is written.
10
+ *
11
+ * Three things here are load-bearing and none is obvious from the happy path:
12
+ *
13
+ * 1. **The search set IS the permission trim.** Every candidate came back from
14
+ * a delegated Microsoft call made as this user. Nothing is added to it,
15
+ * nothing is inferred into it, and no file outside it is ever offered.
16
+ * 2. **A confirmation resolves against SERVER memory, never against what the
17
+ * client sent.** The confirmation carries a position and a nonce; the file
18
+ * it names is looked up in the set this session actually returned. A model
19
+ * that has read a hostile file name cannot conjure a `driveMsId` and have
20
+ * the tool bless it.
21
+ * 3. **The budget is claimed BEFORE the network call.** A cap that discards an
22
+ * answer already fetched has not capped anything — the enumeration already
23
+ * happened, and Microsoft already answered it.
24
+ */
25
+ export declare function registerDriveSearchTool(server: McpServer, api: ApiClient, session?: {
26
+ budget?: SearchBudget;
27
+ registry?: CandidateRegistry;
28
+ }): void;
29
+ /** Re-exported so `InputRequiredResult` is nameable where the tool is used. */
30
+ export type { InputRequiredResult };
31
+ //# sourceMappingURL=drive-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive-search.d.ts","sourceRoot":"","sources":["../../src/tools/drive-search.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EAEf,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,YAAY,EAOb,MAAM,oBAAoB,CAAC;AAsB5B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAAE,GAChE,IAAI,CA8KN;AAiCD,+EAA+E;AAC/E,YAAY,EAAE,mBAAmB,EAAE,CAAC"}