@askthew/mcp-plugin 0.2.2 → 0.2.4

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
@@ -8,6 +8,7 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
8
8
 
9
9
  - Installs an Ask The W MCP server entry into a supported local client.
10
10
  - Preserves existing MCP servers and settings.
11
+ - Adds marked project instructions so future coding-agent sessions know when to send Ask The W updates.
11
12
  - Sends a startup heartbeat so Ask The W can show that the plugin was seen.
12
13
  - Exposes one primary MCP tool: `capture_session_signal`.
13
14
  - Redacts obvious secrets from summaries, evidence excerpts, commands, and metadata before sending.
@@ -58,9 +59,38 @@ npx -y @askthew/mcp-plugin@latest install \
58
59
 
59
60
  After install, restart or reload your coding agent if needed.
60
61
 
62
+ The installer also adds safe, marked project instructions:
63
+
64
+ - Codex: `AGENTS.md`
65
+ - Claude Code: `CLAUDE.md`
66
+ - Cursor: `.cursor/rules/askthew.mdc`
67
+
68
+ These instructions tell the coding agent to send compact Ask The W updates after meaningful direction changes, implementation work, verification, long-session checkpoints, and final summaries. Existing instruction files are preserved.
69
+
70
+ To skip this behavior, pass:
71
+
72
+ ```bash
73
+ --no-agent-instructions
74
+ ```
75
+
61
76
  ## Configuration
62
77
 
63
- The installer writes an MCP server entry like this:
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:
64
94
 
65
95
  ```json
66
96
  {
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { createAskTheWMcpServer } from "./index.js";
4
- import { createHostConfigSnippet, formatInstallCommand, installHostConfig, sendInstallHeartbeat, } from "./install.js";
4
+ import { createHostConfigSnippet, formatInstallCommand, installBehaviorInstructions, installHostConfig, sendInstallHeartbeat, } from "./install.js";
5
5
  function usage() {
6
6
  return [
7
7
  "AskTheW Coding Agent Connector",
8
8
  "",
9
9
  "Usage:",
10
10
  " askthew-mcp",
11
- " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>] [--dry-run]",
11
+ " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>] [--dry-run] [--no-agent-instructions]",
12
12
  " askthew-mcp print-config --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>]",
13
13
  ].join("\n");
14
14
  }
@@ -20,12 +20,17 @@ function parseInstallArgs(argv) {
20
20
  let apiUrl = process.env.ASKTHEW_API_URL?.trim() || "";
21
21
  let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "";
22
22
  let dryRun = false;
23
+ let installAgentInstructions = true;
23
24
  for (let index = 0; index < argv.length; index += 1) {
24
25
  const argument = argv[index];
25
26
  if (argument === "--dry-run") {
26
27
  dryRun = true;
27
28
  continue;
28
29
  }
30
+ if (argument === "--no-agent-instructions") {
31
+ installAgentInstructions = false;
32
+ continue;
33
+ }
29
34
  const next = argv[index + 1];
30
35
  if (!next) {
31
36
  throw new Error(`Missing value for ${argument}.`);
@@ -85,6 +90,7 @@ function parseInstallArgs(argv) {
85
90
  apiUrl,
86
91
  serverName,
87
92
  dryRun,
93
+ installAgentInstructions,
88
94
  };
89
95
  }
90
96
  function normalizeInstallToken(token) {
@@ -105,11 +111,20 @@ async function main() {
105
111
  if (command === "install") {
106
112
  const options = parseInstallArgs(argv);
107
113
  const result = installHostConfig(options);
114
+ const instructions = options.installAgentInstructions
115
+ ? installBehaviorInstructions({
116
+ hostType: options.hostType,
117
+ dryRun: options.dryRun,
118
+ })
119
+ : null;
108
120
  const heartbeatSent = result.wroteFile
109
121
  ? await sendInstallHeartbeat(options).catch(() => false)
110
122
  : false;
111
123
  console.log(result.wroteFile ? "AskTheW plugin install complete." : "AskTheW plugin dry run complete.");
112
124
  console.log(`Settings path: ${result.settingsPath}`);
125
+ if (instructions) {
126
+ console.log(`Agent instructions: ${instructions.path}`);
127
+ }
113
128
  console.log(`Install command: ${formatInstallCommand(options)}`);
114
129
  if (result.wroteFile) {
115
130
  console.log(heartbeatSent
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: {
@@ -66,4 +71,13 @@ export declare function sendInstallHeartbeat(input: HostConfigInput & {
66
71
  cwd?: string;
67
72
  fetchImpl?: typeof fetch;
68
73
  }): Promise<boolean>;
74
+ export declare function installBehaviorInstructions(input: {
75
+ hostType: SupportedHostType;
76
+ cwd?: string;
77
+ dryRun?: boolean;
78
+ }): {
79
+ path: string;
80
+ wroteFile: boolean;
81
+ content: string;
82
+ };
69
83
  export {};
package/dist/install.js CHANGED
@@ -2,13 +2,20 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { resolvePluginScope } from "./scope.js";
5
+ const ASKTHEW_INSTRUCTIONS_START = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_START -->";
6
+ const ASKTHEW_INSTRUCTIONS_END = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_END -->";
5
7
  function isRecord(value) {
6
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
9
  }
8
10
  export function resolveSettingsPath(input) {
9
11
  const homeDirectory = input.homeDirectory ?? os.homedir();
10
- const configDirectory = input.hostType === "claude_code" ? ".claude" : ".codex";
11
- return path.join(homeDirectory, configDirectory, "settings.json");
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");
12
19
  }
13
20
  export function createServerEntry(input) {
14
21
  return {
@@ -25,6 +32,14 @@ export function createServerEntry(input) {
25
32
  };
26
33
  }
27
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
+ }
28
43
  const snippet = {
29
44
  mcpServers: {
30
45
  [input.serverName]: createServerEntry(input),
@@ -36,6 +51,63 @@ export function createHostConfigSnippet(input) {
36
51
  json: JSON.stringify(snippet, null, 2),
37
52
  };
38
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
+ }
39
111
  export function mergeHostSettings(input) {
40
112
  const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
41
113
  const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
@@ -68,8 +140,8 @@ export function formatInstallCommand(input) {
68
140
  ].join(" ");
69
141
  }
70
142
  export function verificationNextStep(hostType) {
71
- const hostLabel = hostType === "claude_code" ? "Claude Code" : "Codex";
72
- return `Refresh Ask The W to confirm the plugin was seen. Restart ${hostLabel} if it is already open, then send a first setup check. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
143
+ const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
144
+ return `Refresh Ask The W to confirm the plugin was seen. Restart ${hostLabel} if it is already open. The installed project instructions tell the coding agent when to send Ask The W updates automatically. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
73
145
  }
74
146
  export function installHostConfig(input) {
75
147
  const settingsPath = resolveSettingsPath({
@@ -77,11 +149,15 @@ export function installHostConfig(input) {
77
149
  homeDirectory: input.homeDirectory,
78
150
  });
79
151
  let existingSettings = {};
152
+ let existingText = "";
80
153
  if (fs.existsSync(settingsPath)) {
81
154
  const raw = fs.readFileSync(settingsPath, "utf8");
155
+ existingText = raw;
82
156
  if (raw.trim().length > 0) {
83
157
  try {
84
- existingSettings = JSON.parse(raw);
158
+ if (input.hostType !== "codex") {
159
+ existingSettings = JSON.parse(raw);
160
+ }
85
161
  }
86
162
  catch (error) {
87
163
  const detail = error instanceof Error ? error.message : "Unknown parse failure.";
@@ -89,7 +165,7 @@ export function installHostConfig(input) {
89
165
  }
90
166
  }
91
167
  }
92
- const merged = mergeHostSettings({
168
+ const hostInput = {
93
169
  existingSettings,
94
170
  hostType: input.hostType,
95
171
  token: input.token,
@@ -97,11 +173,21 @@ export function installHostConfig(input) {
97
173
  serverName: input.serverName,
98
174
  clientId: input.clientId,
99
175
  clientLabel: input.clientLabel,
100
- });
101
- const json = JSON.stringify(merged, null, 2);
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);
102
188
  if (!input.dryRun) {
103
189
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
104
- fs.writeFileSync(settingsPath, `${json}\n`, "utf8");
190
+ fs.writeFileSync(settingsPath, json.endsWith("\n") ? json : `${json}\n`, "utf8");
105
191
  }
106
192
  return {
107
193
  settingsPath,
@@ -133,3 +219,66 @@ export async function sendInstallHeartbeat(input) {
133
219
  });
134
220
  return response.ok;
135
221
  }
222
+ function behaviorInstructions(hostType) {
223
+ const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
224
+ return [
225
+ ASKTHEW_INSTRUCTIONS_START,
226
+ "",
227
+ "## Ask The W Plugin",
228
+ "",
229
+ `This workspace uses the Ask The W Plugin. When working in this repo with ${hostLabel}, send compact Ask The W updates automatically with the MCP tool \`capture_session_signal\`.`,
230
+ "",
231
+ "Send an update:",
232
+ "- after the user accepts or rejects product, architecture, or implementation direction",
233
+ "- after meaningful implementation changes",
234
+ "- after tests, builds, lint, or other verification",
235
+ "- every 8-12 turns in long sessions",
236
+ "- at the final summary",
237
+ "",
238
+ "Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets before sending.",
239
+ "",
240
+ "If this is the first session after install or reload, send a `setup_complete` update first.",
241
+ "",
242
+ ASKTHEW_INSTRUCTIONS_END,
243
+ "",
244
+ ].join("\n");
245
+ }
246
+ function cursorBehaviorInstructions() {
247
+ return [
248
+ "---",
249
+ "description: Ask The W Plugin capture rules",
250
+ "alwaysApply: true",
251
+ "---",
252
+ "",
253
+ behaviorInstructions("cursor"),
254
+ ].join("\n");
255
+ }
256
+ function upsertMarkedBlock(existing, block) {
257
+ const startIndex = existing.indexOf(ASKTHEW_INSTRUCTIONS_START);
258
+ const endIndex = existing.indexOf(ASKTHEW_INSTRUCTIONS_END);
259
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
260
+ const afterEnd = endIndex + ASKTHEW_INSTRUCTIONS_END.length;
261
+ return `${existing.slice(0, startIndex).trimEnd()}\n\n${block.trimEnd()}\n${existing.slice(afterEnd).trimStart()}`.trimEnd() + "\n";
262
+ }
263
+ return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block.trimEnd()}\n`;
264
+ }
265
+ export function installBehaviorInstructions(input) {
266
+ const cwd = path.resolve(input.cwd ?? process.cwd());
267
+ const instructionsPath = input.hostType === "claude_code"
268
+ ? path.join(cwd, "CLAUDE.md")
269
+ : input.hostType === "cursor"
270
+ ? path.join(cwd, ".cursor", "rules", "askthew.mdc")
271
+ : path.join(cwd, "AGENTS.md");
272
+ const block = input.hostType === "cursor" ? cursorBehaviorInstructions() : behaviorInstructions(input.hostType);
273
+ const existing = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, "utf8") : "";
274
+ const next = input.hostType === "cursor" ? block : upsertMarkedBlock(existing, block);
275
+ if (!input.dryRun) {
276
+ fs.mkdirSync(path.dirname(instructionsPath), { recursive: true });
277
+ fs.writeFileSync(instructionsPath, next, "utf8");
278
+ }
279
+ return {
280
+ path: instructionsPath,
281
+ wroteFile: !input.dryRun,
282
+ content: next,
283
+ };
284
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "description": "Ask The W MCP connector for capturing compact coding-agent session signals.",
6
6
  "type": "module",