@ian-pascoe/pi-mcp-selector 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Pi MCP Selector
|
|
2
2
|
|
|
3
|
-
Point the model at MCP servers with
|
|
4
|
-
Type
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Point the model at MCP servers with `&server-name` anywhere in a user message.
|
|
4
|
+
Type `&` to open a completion popup listing your MCP servers. Selecting a server
|
|
5
|
+
leaves shorthand in the editor. Repeat to reference several servers. `@` stays
|
|
6
|
+
Pi's file completion and is not used.
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
pi install npm:@ian-pascoe/pi-mcp-selector
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Requires Pi `>=1.1.0` and Node `>=22.19.0`. There are no settings or extra tools.
|
|
13
|
-
Prose Span detection
|
|
13
|
+
Prose Span detection and the completion popup stack come from
|
|
14
14
|
[`@ian-pascoe/pi-utils/prose-references`](../pi-utils/README.md#prose-references).
|
|
15
15
|
|
|
16
16
|
## Submission
|
|
@@ -18,8 +18,8 @@ Prose Span detection comes from
|
|
|
18
18
|
Typed, pasted, and selected references are equivalent. On submission:
|
|
19
19
|
|
|
20
20
|
```text
|
|
21
|
-
Check open issues with
|
|
22
|
-
→ Check open issues with [
|
|
21
|
+
Check open issues with &github.
|
|
22
|
+
→ Check open issues with [&github](mcp://github "MCP server mcp__github").
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The link target is the configured server name and its title is the namespace
|
|
@@ -30,55 +30,62 @@ prompt, and tool declarations unchanged. The submitted transcript stores the lin
|
|
|
30
30
|
rather than a separate raw shorthand message.
|
|
31
31
|
|
|
32
32
|
Only exact known names in prose convert. Names are case-sensitive, and `-` and
|
|
33
|
-
`_` are interchangeable, as they are in Pi's tool names (
|
|
33
|
+
`_` are interchangeable, as they are in Pi's tool names (`&dev_radius` reaches
|
|
34
34
|
`dev-radius`). These stay literal:
|
|
35
35
|
|
|
36
|
-
- partial or unknown names, `foo
|
|
37
|
-
-
|
|
38
|
-
|
|
36
|
+
- partial or unknown names, `foo&github`, `&&github`, and `\&github`
|
|
37
|
+
- text that merely contains an ampersand: `R&D`, `Q&A`, `a && b`, `Q & A`, and
|
|
38
|
+
Rust's `&self` or `&str` when no server has that name
|
|
39
|
+
- HTML entities, even when a server has the entity's name: `&`, ` `, and
|
|
40
|
+
`&github` stay literal for servers named `amp` or `nbsp` (a name directly
|
|
41
|
+
followed by `;` never converts)
|
|
42
|
+
- anything shaped like a path, address, or URI: `&github/x`, `&github.com`,
|
|
43
|
+
`&./github`, and `&github:resource` (a colon not followed by a space)
|
|
39
44
|
- code, existing Markdown links, and HTML
|
|
40
45
|
|
|
41
|
-
Sentence punctuation does not block a reference:
|
|
42
|
-
|
|
46
|
+
Sentence punctuation does not block a reference: `&github's`, `(&github)`,
|
|
47
|
+
`&github.`, and `&github: list issues` all convert. Reuse of existing links
|
|
43
48
|
is safe, so submitting converted text again changes nothing.
|
|
44
49
|
|
|
45
50
|
## The MCP Server Catalogue
|
|
46
51
|
|
|
47
52
|
Both the popup and conversion read Pi's current tools, fresh on every popup and
|
|
48
|
-
every submission. A server is in the
|
|
53
|
+
every submission. A server is in the Catalogue when it is enabled, connected, and
|
|
49
54
|
has at least one tool that is not `hidden`, whether it comes from `mcp.json` or an
|
|
50
55
|
extension's `registerMcpServer()`. Its exposure (`codemode`, `deferred`, `direct`)
|
|
51
56
|
does not matter, and neither does whether `codemode` is active.
|
|
52
57
|
|
|
53
58
|
- A disabled server re-registers its tools as hidden, so it is absent.
|
|
54
59
|
- A server that is still connecting or needs sign-in has no tools yet, so it is
|
|
55
|
-
absent and its
|
|
60
|
+
absent and its `&name` stays literal, silently. In `pi -p`, the first prompt's
|
|
56
61
|
reference usually stays literal while servers connect.
|
|
57
62
|
- The popup describes each server as `MCP server · <description>`, using the
|
|
58
|
-
configured `description` or the first line of the server's instructions
|
|
59
|
-
`MCP server`
|
|
63
|
+
configured `description` or the first line of the server's instructions
|
|
64
|
+
(without Markdown heading markers such as `# `), or just `MCP server`
|
|
65
|
+
without either.
|
|
60
66
|
|
|
61
|
-
##
|
|
67
|
+
## Completion popup
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
over whatever provider is already installed:
|
|
69
|
+
`&` has its own popup, like `$skill` and `#123` in the sibling selectors:
|
|
65
70
|
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
- `&` after a space or other token boundary, or alone at the start of a line, lists
|
|
72
|
+
every server by name.
|
|
73
|
+
- `&git` lists servers whose name contains `git` (case-insensitive, names that
|
|
74
|
+
start with it first).
|
|
75
|
+
- The popup appears only for an unquoted `&name` in prose, and only when a server
|
|
76
|
+
matches. `&` is plain text in Markdown, so a lone `&` at the start of a line
|
|
77
|
+
opens the popup too. While you type an HTML entity such as `&` the popup
|
|
78
|
+
may list matching servers, and it closes at `;`; the entity never converts.
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
Pi's editor opens the popup by itself only for `@`, `#`, and the characters the
|
|
81
|
+
outermost autocomplete provider lists in `triggerCharacters`. If another extension
|
|
82
|
+
wraps the provider without forwarding `triggerCharacters` (`@ff-labs/pi-fff` 0.11.0
|
|
83
|
+
does not), press Tab after `&` to open it. See
|
|
84
|
+
[ADR 0002](docs/adr/0002-reference-servers-with-ampersand.md) for why `&` and not `@`, `+`, or `$`.
|
|
78
85
|
|
|
79
86
|
## Integration
|
|
80
87
|
|
|
81
|
-
Completion stacks over the current provider, retaining slash and file completion,
|
|
88
|
+
Completion stacks over the current provider, retaining slash and `@` file completion,
|
|
82
89
|
keyboard controls, and custom editors. It runs in the interactive terminal only.
|
|
83
90
|
Conversion applies to interactive, RPC, and print input, including steering and
|
|
84
91
|
follow-ups, without changing attachments or message routing. Extension-generated
|
|
@@ -87,14 +94,14 @@ too.
|
|
|
87
94
|
|
|
88
95
|
## Troubleshooting
|
|
89
96
|
|
|
90
|
-
- **No server in the popup:** open `/mcp` and check the server is enabled and
|
|
97
|
+
- **No server in the popup:** type `&` after a space, then open `/mcp` and check the server is enabled and
|
|
91
98
|
connected, and has a tool that is not hidden. A server that needs sign-in or is
|
|
92
99
|
still connecting is not listed.
|
|
93
100
|
- **No link:** check the exact case-sensitive name (`-` and `_` match either way),
|
|
94
101
|
and whether the reference is escaped, in code, inside a link, or followed by `/`,
|
|
95
|
-
`.name`, or
|
|
96
|
-
-
|
|
97
|
-
|
|
102
|
+
`.name`, `:text`, or `;` (an HTML entity).
|
|
103
|
+
- **`&` opens nothing:** press Tab after `&` (see Completion popup), and check the
|
|
104
|
+
server is in the Catalogue.
|
|
98
105
|
- **Pi older than 1.2.0:** servers are named after their namespace (`dev_radius`),
|
|
99
106
|
not the configured name; `-` and `_` still match either way.
|
|
100
107
|
- **The model did not use the server:** inspect the submitted user message first.
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-mcp-selector
|
|
3
|
-
description: Diagnose Pi MCP Selector when
|
|
3
|
+
description: Diagnose Pi MCP Selector when `&server` autocomplete is missing or an MCP Server Reference does not convert to a link.
|
|
4
4
|
license: MIT
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Pi MCP Selector
|
|
9
9
|
|
|
10
|
-
1. Read [`../../README.md`](../../README.md) for the reference syntax, the MCP Server Catalogue rules, and
|
|
10
|
+
1. Read [`../../README.md`](../../README.md) for the reference syntax, the MCP Server Catalogue rules, and the completion popup.
|
|
11
11
|
2. For a missing server in the popup or a reference that stays literal, check the Catalogue first: `/mcp` must show the server enabled and connected with at least one tool that is not hidden. A server that is connecting, needs sign-in, or is disabled is absent by design. Finish when the server appears in the popup or this named state explains its absence.
|
|
12
|
-
3. For a missing popup, confirm extension loading and interactive mode, then try
|
|
13
|
-
4. For a literal reference with the server present, compare the typed text with the submitted user message and check exact spelling and literal contexts: case,
|
|
12
|
+
3. For a missing popup, confirm extension loading and interactive mode, then try `&` after a space, then `&gi`. If another extension wraps the autocomplete provider without forwarding `triggerCharacters` (pi-fff 0.11.0 does not), press Tab after `&` to open the popup. `@` is Pi's file completion and never lists servers.
|
|
13
|
+
4. For a literal reference with the server present, compare the typed text with the submitted user message and check exact spelling and literal contexts: case, `&name/`, `&name.ext`, `&name:text`, `&name;` (an HTML entity), `R&D`, `a && b`, escapes, code, and links. Finish when the link targets the configured server name or a named boundary explains the mismatch.
|
|
14
14
|
5. If the link is correct but the model did not use the server, report conversion as working: the model controls tool use. Keep this extension configuration-free.
|
|
@@ -10,7 +10,7 @@ export interface McpServer {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export const MCP_SERVER_SIGIL = "
|
|
13
|
+
export const MCP_SERVER_SIGIL = "&";
|
|
14
14
|
|
|
15
15
|
/** Characters a partially typed server name may hold; Pi allows letters, digits, `_`, and `-`. */
|
|
16
16
|
export const MCP_SERVER_PARTIAL_NAME = "[A-Za-z0-9_-]*";
|
|
@@ -21,12 +21,20 @@ const SERVER_NAME = /^[A-Za-z0-9_-]+$/u;
|
|
|
21
21
|
type CatalogueTool = Pick<ToolInfo, "exposure" | "namespace">;
|
|
22
22
|
|
|
23
23
|
/** Hyphen and underscore name the same server, since Pi's namespace folds them together. */
|
|
24
|
-
|
|
24
|
+
const nameKey = (name: string): string => name.replaceAll("-", "_");
|
|
25
25
|
|
|
26
|
+
/** An ATX heading line; group 1 is its text without the opening or closing `#`s. */
|
|
27
|
+
const ATX_HEADING = /^#{1,6}(?:\s+(.*?))?(?:\s+#+)?$/u;
|
|
28
|
+
|
|
29
|
+
/** The first non-empty line, without Markdown ATX heading markers (`# Executor`, `## Executor ##`). */
|
|
26
30
|
const firstLine = (text: string | undefined): string | undefined =>
|
|
27
31
|
text
|
|
28
32
|
?.split("\n")
|
|
29
|
-
.map((line) =>
|
|
33
|
+
.map((line) => {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
const heading = ATX_HEADING.exec(trimmed);
|
|
36
|
+
return heading === null ? trimmed : (heading[1] ?? "").trim();
|
|
37
|
+
})
|
|
30
38
|
.find((line) => line !== "");
|
|
31
39
|
|
|
32
40
|
/**
|
|
@@ -75,9 +83,9 @@ export function matchMcpServers(servers: readonly McpServer[], query: string): M
|
|
|
75
83
|
const escapeRegex = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
84
|
|
|
77
85
|
/**
|
|
78
|
-
* Converts
|
|
86
|
+
* Converts `&server` shorthand for a catalogue server into `[&server](mcp://server "MCP server
|
|
79
87
|
* mcp__server")` inside Prose Spans. Unknown names, code, links, and names that run into a path,
|
|
80
|
-
* domain,
|
|
88
|
+
* domain, `name:text`, or `;` (an HTML entity such as `&`) stay literal.
|
|
81
89
|
*/
|
|
82
90
|
export function transformMcpServerReferences(text: string, servers: readonly McpServer[]): string {
|
|
83
91
|
const catalogue = new Map(servers.map((server) => [nameKey(server.name), server]));
|
|
@@ -88,8 +96,9 @@ export function transformMcpServerReferences(text: string, servers: readonly Mcp
|
|
|
88
96
|
if (names.length === 0) return text;
|
|
89
97
|
return replaceProseReferences(text, {
|
|
90
98
|
sigil: MCP_SERVER_SIGIL,
|
|
91
|
-
// A colon opens a reference only before a space (
|
|
92
|
-
name
|
|
99
|
+
// A colon opens a reference only before a space (`&server: list`); `&server:resource` is a URI.
|
|
100
|
+
// A semicolon right after the name would make an HTML entity (`&`), which stays literal.
|
|
101
|
+
name: `(?:${names.join("|")})(?!:\\S|;)`,
|
|
93
102
|
resolve: (typed) => {
|
|
94
103
|
const server = catalogue.get(nameKey(typed));
|
|
95
104
|
if (server === undefined) return undefined;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
3
|
+
import { stackProseReferenceAutocomplete } from "@ian-pascoe/pi-utils/prose-references";
|
|
4
|
+
import {
|
|
5
|
+
getMcpServerCatalogue,
|
|
6
|
+
MCP_SERVER_PARTIAL_NAME,
|
|
7
|
+
MCP_SERVER_SIGIL,
|
|
8
|
+
matchMcpServers,
|
|
9
|
+
transformMcpServerReferences,
|
|
10
|
+
} from "./mcp-server-references.js";
|
|
4
11
|
|
|
5
12
|
export default function mcpSelectorExtension(pi: ExtensionAPI): void {
|
|
6
13
|
// Read fresh on every use: servers connect, disconnect, and change exposure during a session.
|
|
7
14
|
const getCatalogue = () => getMcpServerCatalogue(pi.getAllTools());
|
|
8
15
|
|
|
16
|
+
const createAutocompleteProvider = (current: AutocompleteProvider): AutocompleteProvider =>
|
|
17
|
+
stackProseReferenceAutocomplete(current, {
|
|
18
|
+
sigil: MCP_SERVER_SIGIL,
|
|
19
|
+
partialName: MCP_SERVER_PARTIAL_NAME,
|
|
20
|
+
suggest: (query) =>
|
|
21
|
+
matchMcpServers(getCatalogue(), query).map((server) => {
|
|
22
|
+
const value = `${MCP_SERVER_SIGIL}${server.name}`;
|
|
23
|
+
return {
|
|
24
|
+
value,
|
|
25
|
+
label: value,
|
|
26
|
+
description:
|
|
27
|
+
server.description === undefined
|
|
28
|
+
? "MCP server"
|
|
29
|
+
: `MCP server · ${server.description}`,
|
|
30
|
+
};
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
9
34
|
pi.on("session_start", (_event, ctx) => {
|
|
10
35
|
if (ctx.mode !== "tui") return;
|
|
11
|
-
ctx.ui.addAutocompleteProvider(
|
|
36
|
+
ctx.ui.addAutocompleteProvider(createAutocompleteProvider);
|
|
12
37
|
});
|
|
13
38
|
|
|
14
39
|
pi.on("input", (event) => {
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type { AutocompleteItem, AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
2
|
-
import { findProseReferencePrefix } from "@ian-pascoe/pi-utils/prose-references";
|
|
3
|
-
import {
|
|
4
|
-
MCP_SERVER_PARTIAL_NAME,
|
|
5
|
-
MCP_SERVER_SIGIL,
|
|
6
|
-
matchMcpServers,
|
|
7
|
-
nameKey,
|
|
8
|
-
type McpServer,
|
|
9
|
-
} from "./mcp-server-references.js";
|
|
10
|
-
|
|
11
|
-
const grammar = { sigil: MCP_SERVER_SIGIL, partialName: MCP_SERVER_PARTIAL_NAME };
|
|
12
|
-
|
|
13
|
-
function prefixAt(lines: string[], cursorLine: number, cursorCol: number): string | null {
|
|
14
|
-
const cursorOffset = lines
|
|
15
|
-
.slice(0, cursorLine)
|
|
16
|
-
.reduce((offset, line) => offset + line.length + 1, cursorCol);
|
|
17
|
-
return findProseReferencePrefix(lines.join("\n"), cursorOffset, grammar);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function serverItem(server: McpServer): AutocompleteItem {
|
|
21
|
-
const value = `${MCP_SERVER_SIGIL}${server.name}`;
|
|
22
|
-
const description =
|
|
23
|
-
server.description === undefined ? "MCP server" : `MCP server · ${server.description}`;
|
|
24
|
-
return { value, label: value, description };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Pi's native `@` completion lists files, so a file named like a server would insert text that
|
|
29
|
-
* converts to a server link. Insert such a file as `@./name` instead.
|
|
30
|
-
*/
|
|
31
|
-
function withoutServerCollision(
|
|
32
|
-
item: AutocompleteItem,
|
|
33
|
-
servers: readonly McpServer[],
|
|
34
|
-
): AutocompleteItem {
|
|
35
|
-
if (!item.value.startsWith(MCP_SERVER_SIGIL)) return item;
|
|
36
|
-
const name = item.value.slice(MCP_SERVER_SIGIL.length);
|
|
37
|
-
const key = nameKey(name);
|
|
38
|
-
if (!servers.some((server) => nameKey(server.name) === key)) return item;
|
|
39
|
-
return { ...item, value: `${MCP_SERVER_SIGIL}./${name}` };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Lists MCP servers above the files Pi's native `@` completion finds, in one popup. The
|
|
44
|
-
* returned provider engages only for an unquoted `@name` prefix in prose while the Catalogue is
|
|
45
|
-
* not empty, and delegates everything else, including applying native items, to `current`. Servers
|
|
46
|
-
* are listed only when they match; native files that collide with a server name are rewritten
|
|
47
|
-
* either way.
|
|
48
|
-
*/
|
|
49
|
-
export function stackMcpServerAutocomplete(
|
|
50
|
-
current: AutocompleteProvider,
|
|
51
|
-
getCatalogue: () => readonly McpServer[],
|
|
52
|
-
): AutocompleteProvider {
|
|
53
|
-
// The editor hands an item back by identity, which is how a server item is told from a file.
|
|
54
|
-
const ownItems = new WeakSet<AutocompleteItem>();
|
|
55
|
-
return {
|
|
56
|
-
triggerCharacters: [...new Set([...(current.triggerCharacters ?? []), MCP_SERVER_SIGIL])],
|
|
57
|
-
async getSuggestions(lines, cursorLine, cursorCol, requestOptions) {
|
|
58
|
-
const delegate = () => current.getSuggestions(lines, cursorLine, cursorCol, requestOptions);
|
|
59
|
-
const prefix = prefixAt(lines, cursorLine, cursorCol);
|
|
60
|
-
if (prefix === null) return delegate();
|
|
61
|
-
const catalogue = getCatalogue();
|
|
62
|
-
if (catalogue.length === 0) return delegate();
|
|
63
|
-
const matches = matchMcpServers(catalogue, prefix.slice(MCP_SERVER_SIGIL.length));
|
|
64
|
-
if (requestOptions.signal.aborted) return null;
|
|
65
|
-
const items = matches.map(serverItem);
|
|
66
|
-
for (const item of items) ownItems.add(item);
|
|
67
|
-
let native: Awaited<ReturnType<AutocompleteProvider["getSuggestions"]>> = null;
|
|
68
|
-
try {
|
|
69
|
-
native = await delegate();
|
|
70
|
-
} catch {
|
|
71
|
-
// Servers still help when file search fails.
|
|
72
|
-
}
|
|
73
|
-
if (requestOptions.signal.aborted) return null;
|
|
74
|
-
if (native === null || native.items.length === 0) {
|
|
75
|
-
return items.length === 0 ? native : { prefix, items };
|
|
76
|
-
}
|
|
77
|
-
if (native.prefix !== prefix) return native;
|
|
78
|
-
// Native search is fuzzy, so a file named like a server can appear for a query that matches
|
|
79
|
-
// no server (`@ghb` finds `github`); it must still insert as `@./name`.
|
|
80
|
-
const files = native.items.map((item) => withoutServerCollision(item, catalogue));
|
|
81
|
-
if (items.length === 0 && files.every((file, index) => file === native.items[index])) {
|
|
82
|
-
return native;
|
|
83
|
-
}
|
|
84
|
-
return { prefix, items: [...items, ...files] };
|
|
85
|
-
},
|
|
86
|
-
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
87
|
-
if (!ownItems.has(item)) {
|
|
88
|
-
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
89
|
-
}
|
|
90
|
-
const line = lines[cursorLine] ?? "";
|
|
91
|
-
const beforePrefix = line.slice(0, cursorCol - prefix.length);
|
|
92
|
-
const completed = [...lines];
|
|
93
|
-
completed[cursorLine] = `${beforePrefix}${item.value} ${line.slice(cursorCol)}`;
|
|
94
|
-
return {
|
|
95
|
-
lines: completed,
|
|
96
|
-
cursorLine,
|
|
97
|
-
cursorCol: beforePrefix.length + item.value.length + 1,
|
|
98
|
-
};
|
|
99
|
-
},
|
|
100
|
-
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
101
|
-
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|