@askthew/mcp-plugin 0.2.3 → 0.2.5
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 +18 -3
- package/dist/cli.js +1 -1
- package/dist/install.d.ts +5 -0
- package/dist/install.js +93 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
|
|
|
9
9
|
- Installs an Ask The W MCP server entry into a supported local client.
|
|
10
10
|
- Preserves existing MCP servers and settings.
|
|
11
11
|
- Adds marked project instructions so future coding-agent sessions know when to send Ask The W updates.
|
|
12
|
-
- Sends a startup heartbeat so Ask The W can show
|
|
12
|
+
- Sends a startup heartbeat so Ask The W can show the plugin as installed.
|
|
13
13
|
- Exposes one primary MCP tool: `capture_session_signal`.
|
|
14
14
|
- Redacts obvious secrets from summaries, evidence excerpts, commands, and metadata before sending.
|
|
15
15
|
- Adds lightweight workspace metadata such as host type, repo name, app path, and server name.
|
|
@@ -75,7 +75,22 @@ To skip this behavior, pass:
|
|
|
75
75
|
|
|
76
76
|
## Configuration
|
|
77
77
|
|
|
78
|
-
The installer writes
|
|
78
|
+
The installer writes the MCP server into the host's native MCP configuration so it appears in that host's MCP server list:
|
|
79
|
+
|
|
80
|
+
- Codex: `~/.codex/config.toml`
|
|
81
|
+
- Claude Code: `~/.claude.json`, scoped to the current project
|
|
82
|
+
- Cursor: `~/.cursor/mcp.json`
|
|
83
|
+
|
|
84
|
+
Codex example:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[mcp_servers.askthew]
|
|
88
|
+
command = "npx"
|
|
89
|
+
args = ["-y", "@askthew/mcp-plugin@latest"]
|
|
90
|
+
env = { ASKTHEW_INSTALL_TOKEN = "<ASKTHEW_INSTALL_TOKEN>", ASKTHEW_HOST_TYPE = "codex", ASKTHEW_API_URL = "https://app.askthew.com/", ASKTHEW_SERVER_NAME = "askthew" }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Claude Code and Cursor use an MCP JSON entry like this:
|
|
79
94
|
|
|
80
95
|
```json
|
|
81
96
|
{
|
|
@@ -125,7 +140,7 @@ Use compact summaries and short evidence excerpts. Do not send full transcripts.
|
|
|
125
140
|
|
|
126
141
|
- Empty `list_mcp_resources` or `list_mcp_resource_templates` results are normal. This connector is tool-driven.
|
|
127
142
|
- If Ask The W shows "Waiting for install", restart or reload your coding agent.
|
|
128
|
-
- If Ask The W shows "Installed",
|
|
143
|
+
- If Ask The W shows "Installed", approve the Ask The W setup popup if your coding agent shows one after reload. The installed behavior instructions handle future compact updates automatically.
|
|
129
144
|
- If a token fails, rotate it in Ask The W and rerun the installer.
|
|
130
145
|
|
|
131
146
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -128,7 +128,7 @@ async function main() {
|
|
|
128
128
|
console.log(`Install command: ${formatInstallCommand(options)}`);
|
|
129
129
|
if (result.wroteFile) {
|
|
130
130
|
console.log(heartbeatSent
|
|
131
|
-
? "Ask The W setup check sent. Refresh the app to confirm the plugin
|
|
131
|
+
? "Ask The W setup check sent. Refresh the app to confirm the plugin shows Installed."
|
|
132
132
|
: "Ask The W setup check could not be sent yet. Restart or reload your coding app, then refresh Ask The W.");
|
|
133
133
|
}
|
|
134
134
|
console.log(`Next step: ${result.nextStep}`);
|
package/dist/install.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface HostConfigInput {
|
|
|
10
10
|
interface InstallHostConfigInput extends HostConfigInput {
|
|
11
11
|
dryRun?: boolean;
|
|
12
12
|
homeDirectory?: string;
|
|
13
|
+
cwd?: string;
|
|
13
14
|
}
|
|
14
15
|
export declare function resolveSettingsPath(input: {
|
|
15
16
|
hostType: SupportedHostType;
|
|
@@ -28,6 +29,10 @@ export declare function createServerEntry(input: HostConfigInput): {
|
|
|
28
29
|
};
|
|
29
30
|
};
|
|
30
31
|
export declare function createHostConfigSnippet(input: HostConfigInput): {
|
|
32
|
+
settingsPath: string;
|
|
33
|
+
snippet: string;
|
|
34
|
+
json: string;
|
|
35
|
+
} | {
|
|
31
36
|
settingsPath: string;
|
|
32
37
|
snippet: {
|
|
33
38
|
mcpServers: {
|
package/dist/install.js
CHANGED
|
@@ -9,8 +9,13 @@ function isRecord(value) {
|
|
|
9
9
|
}
|
|
10
10
|
export function resolveSettingsPath(input) {
|
|
11
11
|
const homeDirectory = input.homeDirectory ?? os.homedir();
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (input.hostType === "codex") {
|
|
13
|
+
return path.join(homeDirectory, ".codex", "config.toml");
|
|
14
|
+
}
|
|
15
|
+
if (input.hostType === "cursor") {
|
|
16
|
+
return path.join(homeDirectory, ".cursor", "mcp.json");
|
|
17
|
+
}
|
|
18
|
+
return path.join(homeDirectory, ".claude.json");
|
|
14
19
|
}
|
|
15
20
|
export function createServerEntry(input) {
|
|
16
21
|
return {
|
|
@@ -27,6 +32,14 @@ export function createServerEntry(input) {
|
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
export function createHostConfigSnippet(input) {
|
|
35
|
+
if (input.hostType === "codex") {
|
|
36
|
+
const toml = createCodexTomlSection(input);
|
|
37
|
+
return {
|
|
38
|
+
settingsPath: resolveSettingsPath({ hostType: input.hostType }),
|
|
39
|
+
snippet: toml,
|
|
40
|
+
json: toml,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
30
43
|
const snippet = {
|
|
31
44
|
mcpServers: {
|
|
32
45
|
[input.serverName]: createServerEntry(input),
|
|
@@ -38,6 +51,63 @@ export function createHostConfigSnippet(input) {
|
|
|
38
51
|
json: JSON.stringify(snippet, null, 2),
|
|
39
52
|
};
|
|
40
53
|
}
|
|
54
|
+
function escapeTomlString(value) {
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
}
|
|
57
|
+
function tomlKey(value) {
|
|
58
|
+
return /^[A-Za-z0-9_-]+$/.test(value) ? value : escapeTomlString(value);
|
|
59
|
+
}
|
|
60
|
+
function createCodexTomlSection(input) {
|
|
61
|
+
const entry = createServerEntry(input);
|
|
62
|
+
const args = entry.args.map(escapeTomlString).join(", ");
|
|
63
|
+
const env = Object.entries(entry.env)
|
|
64
|
+
.map(([key, value]) => `${key} = ${escapeTomlString(String(value))}`)
|
|
65
|
+
.join(", ");
|
|
66
|
+
return [
|
|
67
|
+
`[mcp_servers.${tomlKey(input.serverName)}]`,
|
|
68
|
+
`command = ${escapeTomlString(entry.command)}`,
|
|
69
|
+
`args = [${args}]`,
|
|
70
|
+
`env = { ${env} }`,
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
function removeCodexTomlServer(content, serverName) {
|
|
74
|
+
const escapedServerName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
75
|
+
const quotedServerName = escapeTomlString(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
76
|
+
const sectionPattern = new RegExp(`\\n?\\[mcp_servers\\.(?:${escapedServerName}|${quotedServerName})\\]\\n[\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`, "g");
|
|
77
|
+
return content.replace(sectionPattern, "").trimEnd();
|
|
78
|
+
}
|
|
79
|
+
function mergeCodexSettings(input) {
|
|
80
|
+
let next = input.existingSettings.trimEnd();
|
|
81
|
+
if (input.serverName !== "askthew") {
|
|
82
|
+
next = removeCodexTomlServer(next, "askthew");
|
|
83
|
+
}
|
|
84
|
+
next = removeCodexTomlServer(next, input.serverName);
|
|
85
|
+
return `${next}${next ? "\n\n" : ""}${createCodexTomlSection(input)}\n`;
|
|
86
|
+
}
|
|
87
|
+
function mergeClaudeCodeSettings(input) {
|
|
88
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
89
|
+
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
90
|
+
const existingProjects = isRecord(existingSettings.projects) ? existingSettings.projects : {};
|
|
91
|
+
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
92
|
+
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
93
|
+
const nextMcpServers = { ...existingMcpServers };
|
|
94
|
+
if (input.serverName !== "askthew" && "askthew" in nextMcpServers) {
|
|
95
|
+
delete nextMcpServers.askthew;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
...existingSettings,
|
|
99
|
+
projects: {
|
|
100
|
+
...existingProjects,
|
|
101
|
+
[cwd]: {
|
|
102
|
+
...existingProject,
|
|
103
|
+
mcpServers: {
|
|
104
|
+
...nextMcpServers,
|
|
105
|
+
[input.serverName]: createServerEntry(input),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
41
111
|
export function mergeHostSettings(input) {
|
|
42
112
|
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
43
113
|
const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
|
|
@@ -70,8 +140,8 @@ export function formatInstallCommand(input) {
|
|
|
70
140
|
].join(" ");
|
|
71
141
|
}
|
|
72
142
|
export function verificationNextStep(hostType) {
|
|
73
|
-
const hostLabel = hostType === "claude_code" ? "Claude Code" : "Codex";
|
|
74
|
-
return `Refresh Ask The W to confirm the plugin
|
|
143
|
+
const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
|
|
144
|
+
return `Refresh Ask The W to confirm the plugin shows Installed. Restart or reload ${hostLabel} if it is already open. If ${hostLabel} shows an Ask The W setup popup, approve it once. The installed project instructions tell the coding agent when to send updates automatically. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
|
|
75
145
|
}
|
|
76
146
|
export function installHostConfig(input) {
|
|
77
147
|
const settingsPath = resolveSettingsPath({
|
|
@@ -79,11 +149,15 @@ export function installHostConfig(input) {
|
|
|
79
149
|
homeDirectory: input.homeDirectory,
|
|
80
150
|
});
|
|
81
151
|
let existingSettings = {};
|
|
152
|
+
let existingText = "";
|
|
82
153
|
if (fs.existsSync(settingsPath)) {
|
|
83
154
|
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
155
|
+
existingText = raw;
|
|
84
156
|
if (raw.trim().length > 0) {
|
|
85
157
|
try {
|
|
86
|
-
|
|
158
|
+
if (input.hostType !== "codex") {
|
|
159
|
+
existingSettings = JSON.parse(raw);
|
|
160
|
+
}
|
|
87
161
|
}
|
|
88
162
|
catch (error) {
|
|
89
163
|
const detail = error instanceof Error ? error.message : "Unknown parse failure.";
|
|
@@ -91,7 +165,7 @@ export function installHostConfig(input) {
|
|
|
91
165
|
}
|
|
92
166
|
}
|
|
93
167
|
}
|
|
94
|
-
const
|
|
168
|
+
const hostInput = {
|
|
95
169
|
existingSettings,
|
|
96
170
|
hostType: input.hostType,
|
|
97
171
|
token: input.token,
|
|
@@ -99,11 +173,21 @@ export function installHostConfig(input) {
|
|
|
99
173
|
serverName: input.serverName,
|
|
100
174
|
clientId: input.clientId,
|
|
101
175
|
clientLabel: input.clientLabel,
|
|
102
|
-
}
|
|
103
|
-
const json =
|
|
176
|
+
};
|
|
177
|
+
const json = input.hostType === "codex"
|
|
178
|
+
? mergeCodexSettings({
|
|
179
|
+
...hostInput,
|
|
180
|
+
existingSettings: existingText,
|
|
181
|
+
})
|
|
182
|
+
: JSON.stringify(input.hostType === "claude_code"
|
|
183
|
+
? mergeClaudeCodeSettings({
|
|
184
|
+
...hostInput,
|
|
185
|
+
cwd: input.cwd,
|
|
186
|
+
})
|
|
187
|
+
: mergeHostSettings(hostInput), null, 2);
|
|
104
188
|
if (!input.dryRun) {
|
|
105
189
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
106
|
-
fs.writeFileSync(settingsPath, `${json}\n`, "utf8");
|
|
190
|
+
fs.writeFileSync(settingsPath, json.endsWith("\n") ? json : `${json}\n`, "utf8");
|
|
107
191
|
}
|
|
108
192
|
return {
|
|
109
193
|
settingsPath,
|