@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.
- package/README.md +2 -1
- package/dist/api-client.d.ts +136 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +185 -2
- package/dist/api-client.js.map +1 -1
- package/dist/capabilities.d.ts +5 -6
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +6 -9
- package/dist/capabilities.js.map +1 -1
- package/dist/drive-search.d.ts +162 -0
- package/dist/drive-search.d.ts.map +1 -0
- package/dist/drive-search.js +210 -0
- package/dist/drive-search.js.map +1 -0
- package/dist/enrollment.d.ts +121 -0
- package/dist/enrollment.d.ts.map +1 -0
- package/dist/enrollment.js +247 -0
- package/dist/enrollment.js.map +1 -0
- package/dist/resources/orchestration-guide.md +58 -8
- package/dist/tools/connect-microsoft.d.ts +21 -0
- package/dist/tools/connect-microsoft.d.ts.map +1 -0
- package/dist/tools/connect-microsoft.js +138 -0
- package/dist/tools/connect-microsoft.js.map +1 -0
- package/dist/tools/drive-search-lanes.d.ts +65 -0
- package/dist/tools/drive-search-lanes.d.ts.map +1 -0
- package/dist/tools/drive-search-lanes.js +95 -0
- package/dist/tools/drive-search-lanes.js.map +1 -0
- package/dist/tools/drive-search.contract.d.ts +94 -0
- package/dist/tools/drive-search.contract.d.ts.map +1 -0
- package/dist/tools/drive-search.contract.js +235 -0
- package/dist/tools/drive-search.contract.js.map +1 -0
- package/dist/tools/drive-search.d.ts +31 -0
- package/dist/tools/drive-search.d.ts.map +1 -0
- package/dist/tools/drive-search.js +209 -0
- package/dist/tools/drive-search.js.map +1 -0
- package/dist/tools/enroll-file.contract.d.ts +52 -0
- package/dist/tools/enroll-file.contract.d.ts.map +1 -0
- package/dist/tools/enroll-file.contract.js +105 -0
- package/dist/tools/enroll-file.contract.js.map +1 -0
- package/dist/tools/enroll-file.d.ts +4 -0
- package/dist/tools/enroll-file.d.ts.map +1 -0
- package/dist/tools/enroll-file.js +184 -0
- package/dist/tools/enroll-file.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +12 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +25 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/write-files.d.ts.map +1 -1
- package/dist/tools/write-files.js +5 -0
- package/dist/tools/write-files.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- 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"}
|