@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,247 @@
1
+ /**
2
+ * ENG-2200 (plan 13 / SP03) — the vocabulary and the refusal mapping behind
3
+ * `enroll_file`, kept out of the tool so the branch table can be tested
4
+ * without a registered server and so neither file crosses 300 lines.
5
+ *
6
+ * **Why this tool exists.** A customer asked Claude to enroll a SharePoint
7
+ * workbook. A name-substring search matched a DIFFERENT, already-enrolled file
8
+ * and the assistant answered "already enrolled". Told it had the wrong file,
9
+ * it enumerated its tools and found no enroll tool existed at all (ENG-1647).
10
+ * Two failures: a wrong match reported as a fact, and a dead end. Every
11
+ * decision below is aimed at one of those — an outcome is never inferred from
12
+ * a name, and no path ends without a next action the user can take.
13
+ */
14
+ import { RockhopperApiError } from './api-client.js';
15
+ /**
16
+ * The refusal codes the enrollment routes emit, mirrored rather than imported
17
+ * — this package ships to customers over npm and cannot depend on the backend
18
+ * tree. Sources: `enrollment-access.types.ts` (`EnrollmentAccessDenialCode`)
19
+ * and `graph-url-resolution.types.ts` (`UrlResolutionError`).
20
+ */
21
+ const URL_UNSUPPORTED_PROVIDER = 'URL_UNSUPPORTED_PROVIDER';
22
+ const URL_UNRESOLVABLE = 'URL_UNRESOLVABLE';
23
+ const URL_FOREIGN_TENANT = 'URL_FOREIGN_TENANT';
24
+ const ACCESS_UNPROVEN = 'ACCESS_UNPROVEN';
25
+ const MS_SIGN_IN_REQUIRED = 'MS_SIGN_IN_REQUIRED';
26
+ /** ENG-2614 — the tenant has not approved Rockhopper, and only an
27
+ * administrator can. Its own code because it is the one refusal in this
28
+ * list the user cannot act on themselves. */
29
+ const ADMIN_CONSENT_REQUIRED = 'ADMIN_CONSENT_REQUIRED';
30
+ const FILE_ACCESS_DENIED = 'FILE_ACCESS_DENIED';
31
+ /**
32
+ * There is deliberately NO client-side host allow-list here.
33
+ *
34
+ * The obvious optimisation is to screen `docs.google.com` locally and save a
35
+ * round trip. It was written and removed: the backend's real rule is narrower
36
+ * than the plausible guess — `/(^|\.)sharepoint\.com$/` ONLY, so
37
+ * `onedrive.live.com` and `*.sharepoint.us` are refused there — and only
38
+ * Google earns `unsupported_provider`, while Dropbox and Box are
39
+ * `unresolvable`. A second copy of that rule in a package customers upgrade on
40
+ * their own schedule would answer differently from the server the moment
41
+ * either side moved, and a pre-screen that refuses a link the backend would
42
+ * have taken is invisible: the user simply cannot add their file. One rule,
43
+ * one place; {@link classifyEnrollmentFailure} maps what it says.
44
+ */
45
+ /** The outcome an already-known enrollment state implies, before any write. */
46
+ export function outcomeForState(state) {
47
+ if (state === 'enrolled')
48
+ return 'already_enrolled';
49
+ if (state === 'hidden')
50
+ return 'restore_confirmation_required';
51
+ return null;
52
+ }
53
+ /**
54
+ * ENG-2536 — turn the outcome the SERVER reported into what the user is told.
55
+ *
56
+ * Two things make this worth a function rather than a ternary at the call site.
57
+ *
58
+ * First, it is exhaustive with a `never` arm. `ServerEnrollmentOutcome` is a
59
+ * hand-kept mirror of a backend enum (this package ships over npm and cannot
60
+ * import the backend tree), so the only moment anybody notices a fourth value
61
+ * is when they edit that union — and this is what breaks and makes them handle
62
+ * it. ENG-2580 is the same shape gone wrong: fifteen if-chains where a new
63
+ * value fell into every `else` and nothing failed.
64
+ *
65
+ * Second, `already_enrolled` is the one answer that is not a promise about the
66
+ * future. Everything else here says "we are reading the workbook, come back";
67
+ * that one says nothing was written and nothing will be. Saying "Rockhopper
68
+ * reads the workbook in the background" about a file it read months ago sends
69
+ * the user to wait for an event that will never arrive — the ENG-1647 dead end
70
+ * with a friendlier voice.
71
+ */
72
+ export function describeServerOutcome(serverOutcome, fileName, who) {
73
+ const subject = `"${fileName || 'The workbook'}"`;
74
+ const background = 'Rockhopper reads the workbook in the background, so its versions and ' +
75
+ 'change history appear shortly — check with `search_files` if the user ' +
76
+ 'wants confirmation.';
77
+ switch (serverOutcome) {
78
+ case 'enrolled':
79
+ return {
80
+ outcome: 'enrolled',
81
+ text: `${subject} is being added to Rockhopper${who}. ${background}`,
82
+ };
83
+ case 'restored':
84
+ return {
85
+ outcome: 'restored',
86
+ text: `${subject} is being restored${who}. ${background}`,
87
+ };
88
+ case 'already_enrolled':
89
+ return {
90
+ outcome: 'already_enrolled',
91
+ text: `${subject} was already in Rockhopper, so nothing was added${who}. ` +
92
+ 'Its versions and history are available now through ' +
93
+ '`get_file_versions` and `get_cell_history` — there is nothing to ' +
94
+ 'wait for.',
95
+ };
96
+ default: {
97
+ const exhaustive = serverOutcome;
98
+ return exhaustive;
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Turn a thrown API error into one of the named outcomes.
104
+ *
105
+ * Keyed on the backend's `code` first and the HTTP status only as a fallback,
106
+ * because the codes each name a DIFFERENT remedy and the statuses do not: four
107
+ * distinct refusals share 403, and answering all of them "you do not have
108
+ * permission" sends three of the four users to fix the wrong thing.
109
+ */
110
+ export function classifyEnrollmentFailure(error) {
111
+ if (!(error instanceof RockhopperApiError)) {
112
+ return {
113
+ outcome: 'error',
114
+ message: error instanceof Error ? error.message : String(error),
115
+ };
116
+ }
117
+ // A route this deployment does not serve. The npm package and the backend
118
+ // ship on separate clocks, and a customer running `npx` picks up `latest`
119
+ // the moment it publishes — so calling an unelevated backend is a REAL
120
+ // case, not a theoretical one, and it must not read as "your file is gone".
121
+ if (error.status === 404) {
122
+ return {
123
+ outcome: 'backend_unsupported',
124
+ message: 'This Rockhopper deployment does not support adding files from an ' +
125
+ 'assistant yet. Add the file from the Rockhopper web app instead. ' +
126
+ 'Nothing was changed.',
127
+ };
128
+ }
129
+ switch (error.code) {
130
+ // FIRST, because it is the one refusal here that names an action the
131
+ // user cannot take. Telling them to run `connect_microsoft` sends them
132
+ // to a Microsoft screen that refuses them and returns them here — a
133
+ // loop, and one that reads as our bug rather than a missing approval.
134
+ case ADMIN_CONSENT_REQUIRED:
135
+ return {
136
+ outcome: 'admin_approval_required',
137
+ message: 'Rockhopper cannot add this file because the user\'s organisation ' +
138
+ 'has not approved Rockhopper yet. Microsoft only lets a Microsoft ' +
139
+ '365 administrator approve this — the user cannot grant it ' +
140
+ 'themselves, and connecting or signing in again will not change ' +
141
+ 'the answer. Ask them to send their IT administrator to ' +
142
+ 'https://docs.rockhopper.co/it-setup/approve-file-access, which ' +
143
+ 'has the approval link and what it grants. Nothing was changed, ' +
144
+ 'and retrying will not help.',
145
+ };
146
+ case ACCESS_UNPROVEN:
147
+ return {
148
+ outcome: 'access_unproven',
149
+ message: 'Rockhopper cannot confirm you can open this file, because this ' +
150
+ 'session has no Microsoft account linked to it. Run ' +
151
+ '`connect_microsoft` to link one, then try again. Nothing was ' +
152
+ 'changed.',
153
+ };
154
+ case MS_SIGN_IN_REQUIRED:
155
+ return {
156
+ outcome: 'access_unproven',
157
+ message: 'Your Microsoft sign-in could not be used to confirm you can open ' +
158
+ 'this file. Run `connect_microsoft` to link your account again, ' +
159
+ 'then try again. Nothing was changed.',
160
+ };
161
+ case FILE_ACCESS_DENIED:
162
+ return {
163
+ outcome: 'access_unproven',
164
+ message: 'Microsoft says you cannot open that file, so Rockhopper will not ' +
165
+ 'add it. Ask whoever owns the file to share it with you first. ' +
166
+ 'Retrying will not help.',
167
+ };
168
+ case URL_UNSUPPORTED_PROVIDER:
169
+ return {
170
+ outcome: 'unsupported_provider',
171
+ message: 'That link is not a Microsoft SharePoint or OneDrive file. ' +
172
+ '`enroll_file` adds Microsoft Excel workbooks only. Do not ask for ' +
173
+ 'another link — this provider is not supported.',
174
+ };
175
+ case URL_UNRESOLVABLE:
176
+ return {
177
+ outcome: 'unresolvable',
178
+ message: 'That link does not point at a file Rockhopper can find. Ask the ' +
179
+ 'user to open the workbook in Excel or SharePoint and paste the ' +
180
+ 'address from the browser bar.',
181
+ };
182
+ case URL_FOREIGN_TENANT:
183
+ return {
184
+ outcome: 'unresolvable',
185
+ message: 'That file belongs to a different Microsoft organisation, so ' +
186
+ 'Rockhopper cannot add it. Retrying will not help.',
187
+ };
188
+ default:
189
+ break;
190
+ }
191
+ // No code, so fall back to the status. A 400 from the resolver is always
192
+ // "we could not make sense of this link"; anything else is a real failure
193
+ // and is reported as one rather than dressed up as a named outcome.
194
+ if (error.status === 400) {
195
+ return {
196
+ outcome: 'unresolvable',
197
+ message: 'Rockhopper could not read that link as a file. Ask the user to open ' +
198
+ 'the workbook and paste the address from their browser bar.',
199
+ };
200
+ }
201
+ return { outcome: 'error', message: error.message };
202
+ }
203
+ /** Raised when `share_with: 'team'` names a team we cannot resolve. */
204
+ export class TeamUnresolvedError extends Error {
205
+ constructor(message) {
206
+ super(message);
207
+ this.name = 'TeamUnresolvedError';
208
+ }
209
+ }
210
+ /**
211
+ * The teammates a `share_with: 'team'` enroll fans the file out to.
212
+ *
213
+ * Mirrors the web enrollment wizard exactly — the caller's FIRST team
214
+ * membership, its roster, minus the caller — because SP04 defines "team" as
215
+ * "the wizard's default set" and two surfaces answering the same question
216
+ * differently is worse than either answer. The backend has no team-vs-private
217
+ * enum to lean on: `POST /enrolled-files/batch` takes an explicit list of
218
+ * platform ids and nothing else, so the expansion happens here or nowhere.
219
+ *
220
+ * THROWS rather than returning an empty list when there is no team or no
221
+ * teammate. Enrolling privately after the user said "my team" is a silent
222
+ * substitution of a different answer for the one they gave, and it is
223
+ * invisible: the file simply never appears for anyone else.
224
+ */
225
+ export async function resolveTeamShareTargets(api) {
226
+ const me = await api.getMe();
227
+ const membership = me.teamMembers?.find((m) => m.team != null);
228
+ const teamId = membership?.team?.id ?? membership?.team?.internalId;
229
+ if (teamId === undefined) {
230
+ throw new TeamUnresolvedError('You are not on a team yet, so there is nobody to share this file ' +
231
+ 'with. Call `enroll_file` again with share_with="me" to add it just ' +
232
+ 'for yourself, or set up a team in the Rockhopper web app first. ' +
233
+ 'Nothing was changed.');
234
+ }
235
+ const team = await api.getTeam(teamId);
236
+ const mine = me.msId ?? me.googleId ?? null;
237
+ const targets = (team.teamMembers ?? [])
238
+ .map((member) => member.user?.msId ?? null)
239
+ .filter((id) => !!id && id !== mine);
240
+ if (targets.length === 0) {
241
+ throw new TeamUnresolvedError(`You are the only member of ${team.name ? `the ${team.name} team` : 'your team'}, so sharing with the team would share it with nobody. Call ` +
242
+ '`enroll_file` again with share_with="me" to add it just for ' +
243
+ 'yourself. Nothing was changed.');
244
+ }
245
+ return targets;
246
+ }
247
+ //# sourceMappingURL=enrollment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enrollment.js","sourceRoot":"","sources":["../src/enrollment.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ Rockhopper uses four distinct identifiers. Mixing them is the most common cause
12
12
  | `versionId` | number | `get_file_versions` response (`internalId` field), `create_version` response | Tools that act on a specific version snapshot: `get_reviews`, `create_review_request`, `approve_review`, `cancel_review` |
13
13
  | `versionInternalId` | number | `get_file_comments` response (comments scope to a version) | Comment-thread tools: `add_comment`, `reply_to_comment`, `resolve_comment` |
14
14
  | user / team id | uuid string **or** number | `rockhopper://teams/{teamId}` resource — each record carries an `id` (uuid) and an `internalId` (number) | `reviewerIds` on `create_review_request`; the `{teamId}` in `rockhopper://teams/{teamId}` |
15
+ | `url` / `driveMsId` + `msId` | strings | The user's browser address bar, or a Microsoft file listing | `enroll_file` only — these name a file Rockhopper does **not** have yet, which is why no `fileMsId` exists for it |
15
16
 
16
17
  Decision tree:
17
18
 
@@ -28,7 +29,56 @@ A user or team can be named by its `id` (a uuid, e.g. `0198f3a1-2b4c-7d8e-9f01-2
28
29
 
29
30
  If a tool returns an error like "expected versionId but received fileMsId", you used the wrong identifier — check the tool description for which type it accepts.
30
31
 
31
- ## 2. Reading workflow
32
+ ## 2. Adding a file to Rockhopper (enrollment)
33
+
34
+ Every other tool works on files Rockhopper ALREADY has. `enroll_file` is the only one that adds a new one, and reaching for it is the right move more often than it looks: `list_files` and `search_files` see only enrolled files, so a workbook the user is talking about that appears in neither is very probably one nobody has added yet — not one that does not exist. `search_drive_files` is how you find it.
35
+
36
+ **Identity.** Two ways to name the file, mutually exclusive:
37
+
38
+ - `url` — the SharePoint or OneDrive address, copied from the browser bar. Prefer this always: it names exactly one file, so there is no wrong-match risk.
39
+ - `driveMsId` + `msId` — the Microsoft pair, when another tool already produced it. Both are required together.
40
+
41
+ ### Finding the file first: `search_drive_files` → confirm → `enroll_file`
42
+
43
+ Most users cannot produce a link on request. `search_drive_files` looks across the user's own OneDrive and SharePoint — **including workbooks Rockhopper has never seen** — and marks each candidate `not in Rockhopper yet`, `already in Rockhopper`, or `previously removed`. It is the only tool that can see an un-enrolled file.
44
+
45
+ The two steps are one flow and the middle one is not optional:
46
+
47
+ 1. `search_drive_files({ query: "Q3 forecast" })` — or `scope: "recent"` when the user cannot remember the name.
48
+ 2. **Show the candidates and ask which one they meant.** Quote the names; never choose for them, and never report a file as already enrolled because one result looked similar — that specific mistake is why this tool exists. Call `search_drive_files` again with their `confirm_index` and the `confirm_token` from that answer. Some clients render the question themselves; either way, one comes back confirmed.
49
+ 3. Pass the `driveMsId` + `msId` the confirmation returned to `enroll_file`, which then asks who may see the file.
50
+
51
+ Only files that came back from the search can be confirmed. A pick that names anything else answers `unknown_candidate` and adds nothing.
52
+
53
+ **Searching is capped per session.** A fixed number of searches, after which `search_limit_reached` is the only answer and waiting does not restore it. Search deliberately — two or three well-chosen queries, not a browse. If the cap is reached, ask the user for the workbook link and use `enroll_file` directly.
54
+
55
+ **No Microsoft account connected?** `search_drive_files` answers `microsoft_not_connected` and hands back a sign-in link Rockhopper built. Give the user that link verbatim. Never compose a Microsoft sign-in URL yourself — a link the assistant made up sends the user's consent wherever whoever wrote it wanted.
56
+
57
+ **Microsoft only.** SharePoint and OneDrive-for-Business workbooks. A Google Drive or Sheets link returns `unsupported_provider`; that is final, so do not ask the user for a different link.
58
+
59
+ **You must ask who may see it.** `share_with` is required and has two values: `"me"` (visible to the user alone) and `"team"` (also fanned out to their teammates). Ask the user every time — do not assume, and do not carry an answer over from a previous file. Calling without it enrolls nothing and returns `share_with_required` plus the question to put to them.
60
+
61
+ **A removed file is restored, never re-added.** Rockhopper keeps three states, not two: `enrolled`, `hidden` and `not_enrolled`. `hidden` means somebody deliberately removed the file from the file lists — its versions, comments and change history all survived. Enrolling a hidden file therefore RESTORES the original, and because that undoes a person's decision it needs their say-so: the first call answers `restore_confirmation_required` and writes nothing; only a second call carrying `confirm_restore: true` restores it.
62
+
63
+ **Enrollment is asynchronous, and re-calling is safe.** The answer says the file was accepted, not that it is ready — Rockhopper reads the workbook in the background and its versions appear shortly after. If the answer is lost to a dropped connection mid-call, just call again: the file row is written before the background work starts, so the second call answers `already_enrolled` rather than adding anything twice.
64
+
65
+ **Outcomes.** Every response ends with a JSON object carrying an `outcome` field, so these can be branched on without reading the prose:
66
+
67
+ | `outcome` | Meaning | What to do |
68
+ |-----------|---------|------------|
69
+ | `enrolled` | Accepted; the file was not here before | Tell the user it is being added |
70
+ | `restored` | Accepted; a file they had removed is back | Tell the user it is being restored |
71
+ | `already_enrolled` | It is already here and visible | Nothing — go straight to reading it |
72
+ | `share_with_required` | No `share_with`, or a `"team"` that resolves to nobody | Ask the user, then call again |
73
+ | `restore_confirmation_required` | The target is hidden | Ask the user, then call again with `confirm_restore: true` |
74
+ | `access_unproven` | Rockhopper cannot confirm the user can open the file | Run `connect_microsoft`, then retry |
75
+ | `unresolvable` | The link names no file Rockhopper can find | Ask for the address from the browser bar |
76
+ | `unsupported_provider` | Not a Microsoft link | Stop; do not ask for another link |
77
+ | `backend_unsupported` | This Rockhopper deployment predates the enrollment API | Tell the user to add the file from the web app |
78
+
79
+ **Why `access_unproven` is common from a chat client.** Rockhopper will not add a file on the user's say-so; it checks with Microsoft that they can actually open it. An assistant session carries no Microsoft sign-in of its own, so the user has to link their account once with `connect_microsoft`. Until they do, every enroll is refused — deliberately, not as a bug.
80
+
81
+ ## 3. Reading workflow
32
82
 
33
83
  Standard sequence to inspect a file:
34
84
 
@@ -52,7 +102,7 @@ A refusal means **nothing is known yet**. It is not an empty history, not zero c
52
102
  and not evidence that the file is unmodified. Wait `retryAfterSeconds` and ask again;
53
103
  never answer a user's question about what changed from a not-ready response.
54
104
 
55
- ## 3. Commenting workflow
105
+ ## 4. Commenting workflow
56
106
 
57
107
  Comments are scoped to a version. The default version is the latest **committed** one.
58
108
 
@@ -65,7 +115,7 @@ Comments on historical versions:
65
115
  - Pass `versionInternalId` explicitly to `add_comment` to comment on a historical version snapshot.
66
116
  - `get_file_comments` accepts `versionInternalId` if you want to read historical-version comments rather than the current set.
67
117
 
68
- ## 4. Review lifecycle
118
+ ## 5. Review lifecycle
69
119
 
70
120
  Reviews follow a strict state machine: **`pending` → `approved`** or **`pending` → `cancelled`**. No other transitions are valid.
71
121
 
@@ -75,7 +125,7 @@ Reviews follow a strict state machine: **`pending` → `approved`** or **`pendin
75
125
 
76
126
  Calling `approve_review` before `create_review_request` returns a 404 — there is no review to approve. Calling `approve_review` on an already-approved or cancelled review returns a 409 conflict.
77
127
 
78
- ## 5. Versioning
128
+ ## 6. Versioning
79
129
 
80
130
  `create_version` snapshots the file's current uncommitted state as a new version.
81
131
 
@@ -87,7 +137,7 @@ Rules:
87
137
 
88
138
  `discard_changes(fileMsId)` is the **destructive** alternative: it wipes uncommitted edits without creating a snapshot. Use only when the user explicitly asks to throw away unsaved work — call `get_unattributed_changes(fileMsId)` first and confirm with the user before discarding anything substantive.
89
139
 
90
- ## 6. Uncommitted changes vs. committed history
140
+ ## 7. Uncommitted changes vs. committed history
91
141
 
92
142
  Two distinct concepts. Tools work with one or the other; do not mix.
93
143
 
@@ -96,7 +146,7 @@ Two distinct concepts. Tools work with one or the other; do not mix.
96
146
 
97
147
  Note: on files served from the change ledger (most Microsoft files), `get_cell_history` also includes live edits not yet captured by a committed version — those entries carry the literal versionId `uncommitted`. On files still served from the legacy store, only committed values appear; use `get_unattributed_changes` for pending edits there.
98
148
 
99
- ## 7. Cross-cloud differences
149
+ ## 8. Cross-cloud differences
100
150
 
101
151
  Rockhopper supports both Microsoft Excel files (M365 / OneDrive) and Google Sheets. Tool calls work transparently across both, but the identifiers carry different meanings:
102
152
 
@@ -108,7 +158,7 @@ Rockhopper supports both Microsoft Excel files (M365 / OneDrive) and Google Shee
108
158
 
109
159
  If a tool description references "drive ID" or "platform ID", it works identically across both clouds. If you need to know the platform, inspect `fileType`.
110
160
 
111
- ## 8. Error handling
161
+ ## 9. Error handling
112
162
 
113
163
  Tool failures return structured responses with `isError: true` and a human-readable message in `content`. Common patterns:
114
164
 
@@ -119,7 +169,7 @@ Tool failures return structured responses with `isError: true` and a human-reada
119
169
 
120
170
  When a tool returns `isError: true`, do not silently retry with the same arguments. Either correct the arguments based on the error message or surface the failure.
121
171
 
122
- ## 9. Resources
172
+ ## 10. Resources
123
173
 
124
174
  The MCP server exposes two static resources via `resources/list`:
125
175
 
@@ -0,0 +1,21 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
+ import type { ApiClient } from '../api-client.js';
3
+ /**
4
+ * `connect_microsoft` — the local half of SP07 §3.
5
+ *
6
+ * WHAT THIS TOOL DELIBERATELY CANNOT DO, and why the shape matters more than
7
+ * the feature. It takes NO authorize URL, no redirect URI, no client id and no
8
+ * scope list. It asks the backend to build the consent URL and relays what
9
+ * comes back.
10
+ *
11
+ * The reason is the threat this tool sits inside. An MCP tool's arguments are
12
+ * chosen by a language model, and that model reads content it did not author —
13
+ * file names, comments, review text. A tool that accepted a URL would let
14
+ * anything that model reads steer where a user's Microsoft consent is sent,
15
+ * and the user would see a real Microsoft consent screen the whole way. So the
16
+ * URL is not a parameter, and the callback re-pins the client id and redirect
17
+ * server-side when it redeems the code. Two independent places refuse to take
18
+ * the caller's word for it.
19
+ */
20
+ export declare function registerConnectMicrosoftTool(server: McpServer, api: ApiClient): void;
21
+ //# sourceMappingURL=connect-microsoft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-microsoft.d.ts","sourceRoot":"","sources":["../../src/tools/connect-microsoft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,SAAS,GACb,IAAI,CAgJN"}
@@ -0,0 +1,138 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * `connect_microsoft` — the local half of SP07 §3.
4
+ *
5
+ * WHAT THIS TOOL DELIBERATELY CANNOT DO, and why the shape matters more than
6
+ * the feature. It takes NO authorize URL, no redirect URI, no client id and no
7
+ * scope list. It asks the backend to build the consent URL and relays what
8
+ * comes back.
9
+ *
10
+ * The reason is the threat this tool sits inside. An MCP tool's arguments are
11
+ * chosen by a language model, and that model reads content it did not author —
12
+ * file names, comments, review text. A tool that accepted a URL would let
13
+ * anything that model reads steer where a user's Microsoft consent is sent,
14
+ * and the user would see a real Microsoft consent screen the whole way. So the
15
+ * URL is not a parameter, and the callback re-pins the client id and redirect
16
+ * server-side when it redeems the code. Two independent places refuse to take
17
+ * the caller's word for it.
18
+ */
19
+ export function registerConnectMicrosoftTool(server, api) {
20
+ server.registerTool('connect_microsoft', {
21
+ title: 'Connect Microsoft Account',
22
+ description: 'Start connecting the user\'s Microsoft account so Rockhopper can search ' +
23
+ 'their OneDrive and SharePoint files as them. Returns a Microsoft sign-in ' +
24
+ 'link the USER must open themselves. Call `microsoft_link_status` afterwards ' +
25
+ 'to confirm they finished. Use this when a file search reports that no ' +
26
+ 'Microsoft account is connected.',
27
+ inputSchema: z.object({}),
28
+ annotations: {
29
+ // Creates no Rockhopper data by itself — it hands back a link. The
30
+ // grant only exists once the USER completes the consent.
31
+ readOnlyHint: true,
32
+ // It sends the user to Microsoft.
33
+ openWorldHint: true,
34
+ },
35
+ }, async () => {
36
+ try {
37
+ const handoff = await api.beginMicrosoftConnect();
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: 'Open this link to connect your Microsoft account:\n\n' +
43
+ `${handoff.authorizeUrl}\n\n` +
44
+ `The link expires at ${handoff.expiresAt}. ` +
45
+ 'Rockhopper asks only to READ your files. ' +
46
+ 'After you approve, run `microsoft_link_status` to confirm.',
47
+ },
48
+ ],
49
+ };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ content: [
54
+ {
55
+ type: 'text',
56
+ text: `Failed to start the Microsoft connection: ${error instanceof Error ? error.message : String(error)}`,
57
+ },
58
+ ],
59
+ isError: true,
60
+ };
61
+ }
62
+ });
63
+ server.registerTool('microsoft_link_status', {
64
+ title: 'Microsoft Connection Status',
65
+ description: 'Check whether the user has connected a Microsoft account, which account ' +
66
+ 'it is, and what access was granted. Never returns any token.',
67
+ inputSchema: z.object({}),
68
+ annotations: { readOnlyHint: true, openWorldHint: false },
69
+ }, async () => {
70
+ try {
71
+ const status = await api.getMicrosoftLink();
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: status.linked
77
+ ? `Connected${status.msAccountLabel ? ` as ${status.msAccountLabel}` : ''}.` +
78
+ (status.grantedScopes.length
79
+ ? ` Granted: ${status.grantedScopes.join(', ')}.`
80
+ : '') +
81
+ (status.linkedAt ? ` Connected on ${status.linkedAt}.` : '')
82
+ : 'No Microsoft account is connected. Run `connect_microsoft` to connect one.',
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ catch (error) {
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: `Failed to read the Microsoft connection: ${error instanceof Error ? error.message : String(error)}`,
93
+ },
94
+ ],
95
+ isError: true,
96
+ };
97
+ }
98
+ });
99
+ server.registerTool('disconnect_microsoft', {
100
+ title: 'Disconnect Microsoft Account',
101
+ description: 'Remove the stored Microsoft connection, deleting the credential Rockhopper ' +
102
+ 'holds for this user. Requires an interactive login; a personal access token ' +
103
+ 'cannot sever a connection it did not create.',
104
+ inputSchema: z.object({}),
105
+ annotations: {
106
+ readOnlyHint: false,
107
+ destructiveHint: true,
108
+ idempotentHint: true,
109
+ openWorldHint: false,
110
+ },
111
+ }, async () => {
112
+ try {
113
+ const result = await api.unlinkMicrosoft();
114
+ return {
115
+ content: [
116
+ {
117
+ type: 'text',
118
+ text: result.removed
119
+ ? 'Microsoft account disconnected. The stored credential has been deleted.'
120
+ : 'No Microsoft account was connected.',
121
+ },
122
+ ],
123
+ };
124
+ }
125
+ catch (error) {
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text',
130
+ text: `Failed to disconnect: ${error instanceof Error ? error.message : String(error)}`,
131
+ },
132
+ ],
133
+ isError: true,
134
+ };
135
+ }
136
+ });
137
+ }
138
+ //# sourceMappingURL=connect-microsoft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-microsoft.js","sourceRoot":"","sources":["../../src/tools/connect-microsoft.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
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 { type ServerContext } from '@modelcontextprotocol/server';
22
+ /** The key both richer lanes carry the confirmation under. */
23
+ export declare const CONFIRM_KEY = "confirm_file";
24
+ export type ConfirmationLane = 'input_required' | 'elicitation' | 'tool_result';
25
+ /**
26
+ * Whether this REQUEST is on the 2026-07-28 wire.
27
+ *
28
+ * Read off the request envelope rather than off the connection, because that
29
+ * is where the SDK puts it: `_meta['io.modelcontextprotocol/protocolVersion']`
30
+ * arrives on every modern request, and `stdio-2026-07-28.e2e.test.ts` drives
31
+ * exactly that shape.
32
+ */
33
+ export declare function isModernEra(ctx: ServerContext | undefined): boolean;
34
+ /**
35
+ * Whether the client said it can render an elicitation.
36
+ *
37
+ * Two sources, checked in this order and for different reasons. A modern
38
+ * request carries the client's capabilities in its own envelope, which is the
39
+ * authority for THAT request. A 2025-era session declared them once at
40
+ * `initialize`, and the connection is the only place that survives.
41
+ *
42
+ * ABSENT means NO. A client that never said it can ask a question is not
43
+ * assumed to be able to — the cost of guessing wrong is a call that hangs
44
+ * waiting for an answer nobody will ever be shown.
45
+ */
46
+ export declare function advertisesElicitation(ctx: ServerContext | undefined, connectionCapabilities: Record<string, unknown> | undefined): boolean;
47
+ /**
48
+ * The best rung this request can climb.
49
+ *
50
+ * Lane 2 is deliberately unreachable on the modern era even when the client
51
+ * advertises `elicitation`: the SDK's `ctx.mcpReq.elicitInput` THROWS on a
52
+ * 2026-07-28 request, so treating the capability as sufficient would turn the
53
+ * richest client into the only one that fails.
54
+ */
55
+ export declare function selectLane(ctx: ServerContext | undefined, connectionCapabilities: Record<string, unknown> | undefined): ConfirmationLane;
56
+ /**
57
+ * The nonce a retried modern request is echoing back.
58
+ *
59
+ * Untrusted by construction — it round-trips through the client, nothing signs
60
+ * it, and the SDK hands back whatever arrived. It is used as a Map key and for
61
+ * nothing else: an unrecognised one finds no candidate set, which is the same
62
+ * answer as a malicious one.
63
+ */
64
+ export declare function readRequestState(ctx: ServerContext | undefined): string | null;
65
+ //# sourceMappingURL=drive-search-lanes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive-search-lanes.d.ts","sourceRoot":"","sources":["../../src/tools/drive-search-lanes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAKtC,8DAA8D;AAC9D,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAE1C,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG,aAAa,GAAG,aAAa,CAAC;AAQhF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAEnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,GAAG,SAAS,EAC9B,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1D,OAAO,CAMT;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,aAAa,GAAG,SAAS,EAC9B,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1D,gBAAgB,CAIlB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAS9E"}