@askthew/mcp-plugin 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 ADDED
@@ -0,0 +1,122 @@
1
+ # Ask The W MCP Plugin
2
+
3
+ Connect a local coding agent to Ask The W.
4
+
5
+ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and other MCP-capable tools send compact work-session signals to an Ask The W workspace.
6
+
7
+ ## What It Does
8
+
9
+ - Installs an Ask The W MCP server entry into a supported local client.
10
+ - Preserves existing MCP servers and settings.
11
+ - Sends a startup heartbeat so Ask The W can show that the plugin was seen.
12
+ - Exposes one primary MCP tool: `capture_session_signal`.
13
+ - Redacts obvious secrets from summaries, evidence excerpts, commands, and metadata before sending.
14
+ - Adds lightweight workspace metadata such as host type, repo name, app path, and server name.
15
+
16
+ ## What It Does Not Do
17
+
18
+ - It does not send full transcripts by default.
19
+ - It does not infer decisions locally.
20
+ - It does not link outcomes, score confidence, dedupe signals, or update the graph locally.
21
+ - It does not include the Ask The W app, private server code, Supabase code, or internal analytics code.
22
+
23
+ Ask The W performs inference, linking, approval state, dedupe, and outcome updates in the app.
24
+
25
+ ## Install
26
+
27
+ Create a workspace token in Ask The W, then run the installer from your coding agent or terminal.
28
+
29
+ Codex:
30
+
31
+ ```bash
32
+ npx -y @askthew/mcp-plugin@latest install \
33
+ --host codex \
34
+ --token "<ASKTHEW_INSTALL_TOKEN>" \
35
+ --api-url "https://app.askthew.com/" \
36
+ --server-name "askthew"
37
+ ```
38
+
39
+ Claude Code:
40
+
41
+ ```bash
42
+ npx -y @askthew/mcp-plugin@latest install \
43
+ --host claude_code \
44
+ --token "<ASKTHEW_INSTALL_TOKEN>" \
45
+ --api-url "https://app.askthew.com/" \
46
+ --server-name "askthew"
47
+ ```
48
+
49
+ Cursor:
50
+
51
+ ```bash
52
+ npx -y @askthew/mcp-plugin@latest install \
53
+ --host cursor \
54
+ --token "<ASKTHEW_INSTALL_TOKEN>" \
55
+ --api-url "https://app.askthew.com/" \
56
+ --server-name "askthew"
57
+ ```
58
+
59
+ After install, restart or reload your coding agent if needed.
60
+
61
+ ## Configuration
62
+
63
+ The installer writes an MCP server entry like this:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "askthew": {
69
+ "command": "npx",
70
+ "args": ["-y", "@askthew/mcp-plugin@latest"],
71
+ "env": {
72
+ "ASKTHEW_INSTALL_TOKEN": "<ASKTHEW_INSTALL_TOKEN>",
73
+ "ASKTHEW_HOST_TYPE": "codex",
74
+ "ASKTHEW_API_URL": "https://app.askthew.com/",
75
+ "ASKTHEW_SERVER_NAME": "askthew"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Optional environment variables:
83
+
84
+ - `ASKTHEW_CLIENT_ID`
85
+ - `ASKTHEW_CLIENT_LABEL`
86
+
87
+ ## Tool Contract
88
+
89
+ The public tool surface is intentionally small.
90
+
91
+ ```json
92
+ {
93
+ "name": "capture_session_signal",
94
+ "input": {
95
+ "sessionId": "string",
96
+ "sequence": "number",
97
+ "kind": "setup_complete | session_checkpoint | direction_change | implementation_update | verification_result | final_summary",
98
+ "summary": "string",
99
+ "evidence": [{ "role": "user | assistant | system", "excerpt": "string" }],
100
+ "filesTouched": ["string"],
101
+ "commandsRun": ["string"],
102
+ "metadata": {}
103
+ }
104
+ }
105
+ ```
106
+
107
+ Use compact summaries and short evidence excerpts. Do not send full transcripts.
108
+
109
+ ## Troubleshooting
110
+
111
+ - Empty `list_mcp_resources` or `list_mcp_resource_templates` results are normal. This connector is tool-driven.
112
+ - If Ask The W shows "Waiting for install", restart or reload your coding agent.
113
+ - If Ask The W shows "Installed", ask your coding agent to send the first setup check.
114
+ - If a token fails, rotate it in Ask The W and rerun the installer.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ npm run build --workspace @askthew/mcp-plugin
120
+ npm test --workspace @askthew/mcp-plugin
121
+ npm pack --workspace @askthew/mcp-plugin --dry-run
122
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createAskTheWMcpServer } from "./index.js";
4
+ import { createHostConfigSnippet, formatInstallCommand, installHostConfig, } from "./install.js";
5
+ function usage() {
6
+ return [
7
+ "AskTheW Coding Agent Connector",
8
+ "",
9
+ "Usage:",
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]",
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
+ ].join("\n");
14
+ }
15
+ function parseInstallArgs(argv) {
16
+ let hostType;
17
+ let clientId = process.env.ASKTHEW_CLIENT_ID?.trim() || "";
18
+ let clientLabel = process.env.ASKTHEW_CLIENT_LABEL?.trim() || "";
19
+ let token = normalizeInstallToken(process.env.ASKTHEW_INSTALL_TOKEN) || "";
20
+ let apiUrl = process.env.ASKTHEW_API_URL?.trim() || "";
21
+ let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "";
22
+ let dryRun = false;
23
+ for (let index = 0; index < argv.length; index += 1) {
24
+ const argument = argv[index];
25
+ if (argument === "--dry-run") {
26
+ dryRun = true;
27
+ continue;
28
+ }
29
+ const next = argv[index + 1];
30
+ if (!next) {
31
+ throw new Error(`Missing value for ${argument}.`);
32
+ }
33
+ if (argument === "--host") {
34
+ if (next !== "claude_code" && next !== "codex" && next !== "cursor") {
35
+ throw new Error(`Unsupported host "${next}". Expected claude_code, codex, or cursor.`);
36
+ }
37
+ hostType = next;
38
+ index += 1;
39
+ continue;
40
+ }
41
+ if (argument === "--client-id") {
42
+ clientId = next;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (argument === "--client-label") {
47
+ clientLabel = next;
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (argument === "--token") {
52
+ token = normalizeInstallToken(next);
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (argument === "--api-url") {
57
+ apiUrl = next;
58
+ index += 1;
59
+ continue;
60
+ }
61
+ if (argument === "--server-name") {
62
+ serverName = next;
63
+ index += 1;
64
+ continue;
65
+ }
66
+ throw new Error(`Unknown argument: ${argument}`);
67
+ }
68
+ if (!hostType) {
69
+ throw new Error("Missing required --host argument.");
70
+ }
71
+ if (!token) {
72
+ throw new Error("Missing required --token argument.");
73
+ }
74
+ if (!apiUrl) {
75
+ throw new Error("Missing required --api-url argument.");
76
+ }
77
+ if (!serverName) {
78
+ throw new Error("Missing required --server-name argument.");
79
+ }
80
+ return {
81
+ hostType,
82
+ clientId: clientId || undefined,
83
+ clientLabel: clientLabel || undefined,
84
+ token,
85
+ apiUrl,
86
+ serverName,
87
+ dryRun,
88
+ };
89
+ }
90
+ function normalizeInstallToken(token) {
91
+ return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
92
+ }
93
+ async function main() {
94
+ const [command, ...argv] = process.argv.slice(2);
95
+ if (command === "--help" || command === "-h" || command === "help") {
96
+ console.log(usage());
97
+ return;
98
+ }
99
+ if (command === "print-config") {
100
+ const options = parseInstallArgs(argv);
101
+ const snippet = createHostConfigSnippet(options);
102
+ console.log(snippet.json);
103
+ return;
104
+ }
105
+ if (command === "install") {
106
+ const options = parseInstallArgs(argv);
107
+ const result = installHostConfig(options);
108
+ console.log(result.wroteFile ? "AskTheW plugin install complete." : "AskTheW plugin dry run complete.");
109
+ console.log(`Settings path: ${result.settingsPath}`);
110
+ console.log(`Install command: ${formatInstallCommand(options)}`);
111
+ console.log(`Next step: ${result.nextStep}`);
112
+ if (!result.wroteFile) {
113
+ console.log("");
114
+ console.log(result.json);
115
+ }
116
+ return;
117
+ }
118
+ if (command) {
119
+ throw new Error(`Unknown command "${command}".\n\n${usage()}`);
120
+ }
121
+ const server = createAskTheWMcpServer();
122
+ const transport = new StdioServerTransport();
123
+ await server.connect(transport);
124
+ }
125
+ main().catch((error) => {
126
+ if (error instanceof Error) {
127
+ console.error(error.message);
128
+ }
129
+ else {
130
+ console.error("AskTheW plugin failed to start.", error);
131
+ }
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,115 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ export declare const codingSessionSignalSchema: z.ZodObject<{
4
+ sessionId: z.ZodString;
5
+ sequence: z.ZodNumber;
6
+ kind: z.ZodEnum<["setup_complete", "session_checkpoint", "direction_change", "implementation_update", "verification_result", "final_summary"]>;
7
+ summary: z.ZodString;
8
+ evidence: z.ZodDefault<z.ZodArray<z.ZodObject<{
9
+ role: z.ZodEnum<["user", "assistant", "system"]>;
10
+ excerpt: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ role: "user" | "assistant" | "system";
13
+ excerpt: string;
14
+ }, {
15
+ role: "user" | "assistant" | "system";
16
+ excerpt: string;
17
+ }>, "many">>;
18
+ filesTouched: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
+ commandsRun: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
20
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
21
+ }, "strip", z.ZodTypeAny, {
22
+ sessionId: string;
23
+ sequence: number;
24
+ kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
25
+ summary: string;
26
+ evidence: {
27
+ role: "user" | "assistant" | "system";
28
+ excerpt: string;
29
+ }[];
30
+ filesTouched: string[];
31
+ commandsRun: string[];
32
+ metadata: Record<string, unknown>;
33
+ }, {
34
+ sessionId: string;
35
+ sequence: number;
36
+ kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
37
+ summary: string;
38
+ evidence?: {
39
+ role: "user" | "assistant" | "system";
40
+ excerpt: string;
41
+ }[] | undefined;
42
+ filesTouched?: string[] | undefined;
43
+ commandsRun?: string[] | undefined;
44
+ metadata?: Record<string, unknown> | undefined;
45
+ }>;
46
+ export type CodingSessionSignal = z.infer<typeof codingSessionSignalSchema>;
47
+ export declare const provenanceSignalSchema: z.ZodObject<{
48
+ source: z.ZodString;
49
+ decision: z.ZodString;
50
+ rationale: z.ZodString;
51
+ confidence: z.ZodNumber;
52
+ framework: z.ZodOptional<z.ZodString>;
53
+ filesAffected: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
54
+ sessionId: z.ZodString;
55
+ pendingApproval: z.ZodOptional<z.ZodBoolean>;
56
+ originatingPrompt: z.ZodOptional<z.ZodString>;
57
+ installToken: z.ZodOptional<z.ZodString>;
58
+ metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
59
+ }, "strip", z.ZodTypeAny, {
60
+ sessionId: string;
61
+ metadata: Record<string, unknown>;
62
+ source: string;
63
+ decision: string;
64
+ rationale: string;
65
+ confidence: number;
66
+ filesAffected: string[];
67
+ framework?: string | undefined;
68
+ pendingApproval?: boolean | undefined;
69
+ originatingPrompt?: string | undefined;
70
+ installToken?: string | undefined;
71
+ }, {
72
+ sessionId: string;
73
+ source: string;
74
+ decision: string;
75
+ rationale: string;
76
+ confidence: number;
77
+ metadata?: Record<string, unknown> | undefined;
78
+ framework?: string | undefined;
79
+ filesAffected?: string[] | undefined;
80
+ pendingApproval?: boolean | undefined;
81
+ originatingPrompt?: string | undefined;
82
+ installToken?: string | undefined;
83
+ }>;
84
+ export type ProvenanceSignal = z.infer<typeof provenanceSignalSchema>;
85
+ export declare function inferFunctionalArea(signal: Pick<ProvenanceSignal, "filesAffected">): string;
86
+ export declare function redactProvenanceSignal(input: ProvenanceSignal): {
87
+ originatingPrompt: string | undefined;
88
+ metadata: {
89
+ functional_area: string;
90
+ };
91
+ sessionId: string;
92
+ source: string;
93
+ decision: string;
94
+ rationale: string;
95
+ confidence: number;
96
+ filesAffected: string[];
97
+ framework?: string | undefined;
98
+ pendingApproval?: boolean | undefined;
99
+ installToken?: string | undefined;
100
+ };
101
+ export declare function redactCodingSessionSignal(input: CodingSessionSignal): {
102
+ summary: string;
103
+ evidence: {
104
+ excerpt: string;
105
+ role: "user" | "assistant" | "system";
106
+ }[];
107
+ commandsRun: string[];
108
+ metadata: Record<string, unknown>;
109
+ sessionId: string;
110
+ sequence: number;
111
+ kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
112
+ filesTouched: string[];
113
+ };
114
+ export declare function normalizeInstallTokenInput(token: string | undefined): string;
115
+ export declare function createAskTheWMcpServer(): McpServer;
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { resolveFunctionalAreaFromToml, resolvePluginScope } from "./scope.js";
6
+ const evidenceRoleSchema = z.enum(["user", "assistant", "system"]);
7
+ const sessionSignalKindSchema = z.enum([
8
+ "setup_complete",
9
+ "session_checkpoint",
10
+ "direction_change",
11
+ "implementation_update",
12
+ "verification_result",
13
+ "final_summary",
14
+ ]);
15
+ export const codingSessionSignalSchema = z.object({
16
+ sessionId: z.string().min(1),
17
+ sequence: z.number().int().nonnegative(),
18
+ kind: sessionSignalKindSchema,
19
+ summary: z.string().min(1).max(2000),
20
+ evidence: z
21
+ .array(z.object({
22
+ role: evidenceRoleSchema,
23
+ excerpt: z.string().min(1).max(500),
24
+ }))
25
+ .default([]),
26
+ filesTouched: z.array(z.string().min(1).max(500)).default([]),
27
+ commandsRun: z.array(z.string().min(1).max(500)).default([]),
28
+ metadata: z.record(z.string(), z.unknown()).default({}),
29
+ });
30
+ export const provenanceSignalSchema = z.object({
31
+ source: z.string().min(1),
32
+ decision: z.string().min(1),
33
+ rationale: z.string().min(1),
34
+ confidence: z.number().min(0).max(1),
35
+ framework: z.string().optional(),
36
+ filesAffected: z.array(z.string()).default([]),
37
+ sessionId: z.string().min(1),
38
+ pendingApproval: z.boolean().optional(),
39
+ originatingPrompt: z.string().optional(),
40
+ installToken: z.string().optional(),
41
+ metadata: z.record(z.string(), z.unknown()).default({}),
42
+ });
43
+ const REDACTION_PATTERNS = [
44
+ /\bAKIA[0-9A-Z]{16}\b/g,
45
+ /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
46
+ /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g,
47
+ /\b[A-Za-z0-9_-]{32,}\b/g,
48
+ ];
49
+ function redactSecrets(text) {
50
+ return REDACTION_PATTERNS.reduce((accumulator, pattern) => accumulator.replace(pattern, "[REDACTED]"), text);
51
+ }
52
+ function redactMetadata(value) {
53
+ if (typeof value === "string") {
54
+ return redactSecrets(value);
55
+ }
56
+ if (Array.isArray(value)) {
57
+ return value.map((entry) => redactMetadata(entry));
58
+ }
59
+ if (typeof value === "object" && value !== null) {
60
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactMetadata(entry)]));
61
+ }
62
+ return value;
63
+ }
64
+ export function inferFunctionalArea(signal) {
65
+ const explicit = process.env.ASKTHEW_FUNCTIONAL_AREA?.trim();
66
+ if (explicit) {
67
+ return explicit;
68
+ }
69
+ const configuredArea = resolveFunctionalAreaFromToml(process.cwd());
70
+ if (configuredArea) {
71
+ return configuredArea;
72
+ }
73
+ const files = (signal.filesAffected ?? []).map((file) => file.toLowerCase());
74
+ if (files.length > 0 && files.every((file) => file.includes("test"))) {
75
+ return "QA";
76
+ }
77
+ if (files.length > 0 && files.every((file) => file.includes("marketing") || file.includes("content"))) {
78
+ return "Marketing";
79
+ }
80
+ if (files.length > 0 && files.every((file) => file.includes("design"))) {
81
+ return "Design";
82
+ }
83
+ if (fs.existsSync(path.resolve(process.cwd(), "figma.config")) || fs.existsSync(path.resolve(process.cwd(), ".sketch"))) {
84
+ return "Design";
85
+ }
86
+ if (fs.existsSync(path.resolve(process.cwd(), "campaign.yml"))) {
87
+ return "Marketing";
88
+ }
89
+ const repoMarkers = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml"];
90
+ if (repoMarkers.some((marker) => fs.existsSync(path.resolve(process.cwd(), marker)))) {
91
+ return "Engineering";
92
+ }
93
+ return "Engineering";
94
+ }
95
+ export function redactProvenanceSignal(input) {
96
+ const parsed = provenanceSignalSchema.parse(input);
97
+ return {
98
+ ...parsed,
99
+ originatingPrompt: typeof parsed.originatingPrompt === "string"
100
+ ? redactSecrets(parsed.originatingPrompt)
101
+ : undefined,
102
+ metadata: {
103
+ ...parsed.metadata,
104
+ functional_area: inferFunctionalArea(parsed),
105
+ },
106
+ };
107
+ }
108
+ export function redactCodingSessionSignal(input) {
109
+ const parsed = codingSessionSignalSchema.parse(input);
110
+ return {
111
+ ...parsed,
112
+ summary: redactSecrets(parsed.summary),
113
+ evidence: parsed.evidence.map((entry) => ({
114
+ ...entry,
115
+ excerpt: redactSecrets(entry.excerpt),
116
+ })),
117
+ commandsRun: parsed.commandsRun.map((command) => redactSecrets(command)),
118
+ metadata: redactMetadata(parsed.metadata),
119
+ };
120
+ }
121
+ function apiBaseUrl() {
122
+ return process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com";
123
+ }
124
+ function normalizeClientId(value) {
125
+ return String(value ?? "")
126
+ .trim()
127
+ .toLowerCase()
128
+ .replace(/[^a-z0-9]+/g, "_")
129
+ .replace(/^_+|_+$/g, "")
130
+ .slice(0, 64);
131
+ }
132
+ export function normalizeInstallTokenInput(token) {
133
+ return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
134
+ }
135
+ function credentials() {
136
+ const legacyHostType = process.env.ASKTHEW_HOST_TYPE?.trim();
137
+ const hostType = legacyHostType === "claude_code" || legacyHostType === "codex" || legacyHostType === "cursor"
138
+ ? legacyHostType
139
+ : undefined;
140
+ const clientId = normalizeClientId(process.env.ASKTHEW_CLIENT_ID?.trim()) || hostType || "mcp_client";
141
+ return {
142
+ installToken: normalizeInstallTokenInput(process.env.ASKTHEW_INSTALL_TOKEN),
143
+ userId: process.env.ASKTHEW_USER_ID?.trim(),
144
+ apiKey: process.env.ASKTHEW_API_KEY?.trim(),
145
+ serverName: process.env.ASKTHEW_SERVER_NAME?.trim(),
146
+ clientId,
147
+ clientLabel: process.env.ASKTHEW_CLIENT_LABEL?.trim(),
148
+ hostType,
149
+ };
150
+ }
151
+ function hasServerIdentity() {
152
+ const { installToken, userId } = credentials();
153
+ return Boolean(installToken || userId);
154
+ }
155
+ async function postToServer(route, payload) {
156
+ if (!hasServerIdentity()) {
157
+ return null;
158
+ }
159
+ const { installToken, userId, apiKey, clientId, clientLabel, hostType } = credentials();
160
+ const response = await fetch(`${apiBaseUrl()}${route}`, {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
165
+ },
166
+ body: JSON.stringify({
167
+ ...payload,
168
+ installToken: installToken || undefined,
169
+ userId: userId || undefined,
170
+ clientId,
171
+ clientLabel: clientLabel || undefined,
172
+ hostType: hostType || undefined,
173
+ }),
174
+ }).catch(() => null);
175
+ if (!response || !response.ok) {
176
+ return null;
177
+ }
178
+ return response.json().catch(() => null);
179
+ }
180
+ function runtimeMetadata() {
181
+ const scope = resolvePluginScope(process.cwd());
182
+ const { serverName, clientId, clientLabel } = credentials();
183
+ return {
184
+ repository: scope.repoName,
185
+ repo_name: scope.repoName,
186
+ ...(scope.repoRoot ? { repo_root: scope.repoRoot } : {}),
187
+ ...(scope.appPath ? { app_path: scope.appPath } : {}),
188
+ ...(scope.serviceName ? { service_name: scope.serviceName } : {}),
189
+ ...(serverName ? { server_name: serverName } : {}),
190
+ ...(clientId ? { client_id: clientId } : {}),
191
+ ...(clientLabel ? { client_label: clientLabel } : {}),
192
+ };
193
+ }
194
+ async function sendStartupHeartbeat() {
195
+ if (!hasServerIdentity()) {
196
+ return;
197
+ }
198
+ const { installToken, apiKey, clientId, clientLabel, hostType, serverName } = credentials();
199
+ if (!installToken) {
200
+ return;
201
+ }
202
+ const scope = resolvePluginScope(process.cwd());
203
+ await fetch(`${apiBaseUrl()}/api/connectors/mcp/heartbeat`, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
208
+ },
209
+ body: JSON.stringify({
210
+ installToken,
211
+ clientId,
212
+ ...(clientLabel ? { clientLabel } : {}),
213
+ hostType,
214
+ ...(serverName ? { serverName } : {}),
215
+ repoName: scope.repoName,
216
+ ...(scope.repoRoot ? { repoRoot: scope.repoRoot } : {}),
217
+ ...(scope.appPath ? { appPath: scope.appPath } : {}),
218
+ ...(scope.serviceName ? { serviceName: scope.serviceName } : {}),
219
+ }),
220
+ }).catch(() => null);
221
+ }
222
+ export function createAskTheWMcpServer() {
223
+ const server = new McpServer({
224
+ name: "AskTheW Coding Agent Connector",
225
+ version: "1.0.0",
226
+ });
227
+ void sendStartupHeartbeat();
228
+ server.tool("capture_session_signal", {
229
+ sessionId: z.string().min(1),
230
+ sequence: z.number().int().nonnegative(),
231
+ kind: sessionSignalKindSchema,
232
+ summary: z.string().min(1).max(2000),
233
+ evidence: z
234
+ .array(z.object({
235
+ role: evidenceRoleSchema,
236
+ excerpt: z.string().min(1).max(500),
237
+ }))
238
+ .default([]),
239
+ filesTouched: z.array(z.string().min(1).max(500)).default([]),
240
+ commandsRun: z.array(z.string().min(1).max(500)).default([]),
241
+ metadata: z.record(z.string(), z.unknown()).default({}),
242
+ }, async (payload) => {
243
+ const sessionSignal = redactCodingSessionSignal({
244
+ ...payload,
245
+ metadata: {
246
+ ...runtimeMetadata(),
247
+ ...(payload.metadata ?? {}),
248
+ },
249
+ });
250
+ const upstream = await postToServer("/api/ingest/mcp", {
251
+ sessionSignal,
252
+ });
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: JSON.stringify({
258
+ ok: true,
259
+ signal: sessionSignal,
260
+ upstream,
261
+ note: "Ask The W stores this as source material and performs inference in the app.",
262
+ }, null, 2),
263
+ },
264
+ ],
265
+ };
266
+ });
267
+ return server;
268
+ }
@@ -0,0 +1,65 @@
1
+ export type SupportedHostType = "claude_code" | "codex" | "cursor";
2
+ interface HostConfigInput {
3
+ hostType: SupportedHostType;
4
+ token: string;
5
+ apiUrl: string;
6
+ serverName: string;
7
+ clientId?: string;
8
+ clientLabel?: string;
9
+ }
10
+ interface InstallHostConfigInput extends HostConfigInput {
11
+ dryRun?: boolean;
12
+ homeDirectory?: string;
13
+ }
14
+ export declare function resolveSettingsPath(input: {
15
+ hostType: SupportedHostType;
16
+ homeDirectory?: string;
17
+ }): string;
18
+ export declare function createServerEntry(input: HostConfigInput): {
19
+ command: string;
20
+ args: string[];
21
+ env: {
22
+ ASKTHEW_HOST_TYPE: SupportedHostType;
23
+ ASKTHEW_SERVER_NAME: string;
24
+ ASKTHEW_CLIENT_LABEL?: string | undefined;
25
+ ASKTHEW_CLIENT_ID?: string | undefined;
26
+ ASKTHEW_INSTALL_TOKEN: string;
27
+ ASKTHEW_API_URL: string;
28
+ };
29
+ };
30
+ export declare function createHostConfigSnippet(input: HostConfigInput): {
31
+ settingsPath: string;
32
+ snippet: {
33
+ mcpServers: {
34
+ [input.serverName]: {
35
+ command: string;
36
+ args: string[];
37
+ env: {
38
+ ASKTHEW_HOST_TYPE: SupportedHostType;
39
+ ASKTHEW_SERVER_NAME: string;
40
+ ASKTHEW_CLIENT_LABEL?: string | undefined;
41
+ ASKTHEW_CLIENT_ID?: string | undefined;
42
+ ASKTHEW_INSTALL_TOKEN: string;
43
+ ASKTHEW_API_URL: string;
44
+ };
45
+ };
46
+ };
47
+ };
48
+ json: string;
49
+ };
50
+ export declare function mergeHostSettings(input: HostConfigInput & {
51
+ existingSettings: unknown;
52
+ }): {
53
+ mcpServers: {
54
+ [x: string]: unknown;
55
+ };
56
+ };
57
+ export declare function formatInstallCommand(input: HostConfigInput): string;
58
+ export declare function verificationNextStep(hostType: SupportedHostType): string;
59
+ export declare function installHostConfig(input: InstallHostConfigInput): {
60
+ settingsPath: string;
61
+ json: string;
62
+ wroteFile: boolean;
63
+ nextStep: string;
64
+ };
65
+ export {};
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ export function resolveSettingsPath(input) {
8
+ const homeDirectory = input.homeDirectory ?? os.homedir();
9
+ const configDirectory = input.hostType === "claude_code" ? ".claude" : ".codex";
10
+ return path.join(homeDirectory, configDirectory, "settings.json");
11
+ }
12
+ export function createServerEntry(input) {
13
+ return {
14
+ command: "npx",
15
+ args: ["-y", "@askthew/mcp-plugin@latest"],
16
+ env: {
17
+ ASKTHEW_INSTALL_TOKEN: input.token,
18
+ ASKTHEW_API_URL: input.apiUrl,
19
+ ...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
20
+ ...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
21
+ ASKTHEW_HOST_TYPE: input.hostType,
22
+ ASKTHEW_SERVER_NAME: input.serverName,
23
+ },
24
+ };
25
+ }
26
+ export function createHostConfigSnippet(input) {
27
+ const snippet = {
28
+ mcpServers: {
29
+ [input.serverName]: createServerEntry(input),
30
+ },
31
+ };
32
+ return {
33
+ settingsPath: resolveSettingsPath({ hostType: input.hostType }),
34
+ snippet,
35
+ json: JSON.stringify(snippet, null, 2),
36
+ };
37
+ }
38
+ export function mergeHostSettings(input) {
39
+ const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
40
+ const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
41
+ const nextMcpServers = { ...existingMcpServers };
42
+ if (input.serverName !== "askthew" && "askthew" in nextMcpServers) {
43
+ delete nextMcpServers.askthew;
44
+ }
45
+ return {
46
+ ...existingSettings,
47
+ mcpServers: {
48
+ ...nextMcpServers,
49
+ [input.serverName]: createServerEntry(input),
50
+ },
51
+ };
52
+ }
53
+ export function formatInstallCommand(input) {
54
+ return [
55
+ "npx",
56
+ "-y",
57
+ "@askthew/mcp-plugin@latest",
58
+ "install",
59
+ "--host",
60
+ input.hostType,
61
+ "--token",
62
+ JSON.stringify(input.token),
63
+ "--api-url",
64
+ JSON.stringify(input.apiUrl),
65
+ "--server-name",
66
+ JSON.stringify(input.serverName),
67
+ ].join(" ");
68
+ }
69
+ export function verificationNextStep(hostType) {
70
+ const hostLabel = hostType === "claude_code" ? "Claude Code" : "Codex";
71
+ return `Restart ${hostLabel} if it is already open, then send a setup_complete signal with capture_session_signal. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
72
+ }
73
+ export function installHostConfig(input) {
74
+ const settingsPath = resolveSettingsPath({
75
+ hostType: input.hostType,
76
+ homeDirectory: input.homeDirectory,
77
+ });
78
+ let existingSettings = {};
79
+ if (fs.existsSync(settingsPath)) {
80
+ const raw = fs.readFileSync(settingsPath, "utf8");
81
+ if (raw.trim().length > 0) {
82
+ try {
83
+ existingSettings = JSON.parse(raw);
84
+ }
85
+ catch (error) {
86
+ const detail = error instanceof Error ? error.message : "Unknown parse failure.";
87
+ throw new Error(`Could not parse existing settings JSON at ${settingsPath}: ${detail}`);
88
+ }
89
+ }
90
+ }
91
+ const merged = mergeHostSettings({
92
+ existingSettings,
93
+ hostType: input.hostType,
94
+ token: input.token,
95
+ apiUrl: input.apiUrl,
96
+ serverName: input.serverName,
97
+ clientId: input.clientId,
98
+ clientLabel: input.clientLabel,
99
+ });
100
+ const json = JSON.stringify(merged, null, 2);
101
+ if (!input.dryRun) {
102
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
103
+ fs.writeFileSync(settingsPath, `${json}\n`, "utf8");
104
+ }
105
+ return {
106
+ settingsPath,
107
+ json,
108
+ wroteFile: !input.dryRun,
109
+ nextStep: verificationNextStep(input.hostType),
110
+ };
111
+ }
@@ -0,0 +1,8 @@
1
+ export interface PluginScope {
2
+ repoName: string;
3
+ repoRoot?: string;
4
+ appPath?: string;
5
+ serviceName?: string;
6
+ }
7
+ export declare function resolvePluginScope(startDirectory?: string): PluginScope;
8
+ export declare function resolveFunctionalAreaFromToml(startDirectory?: string): string;
package/dist/scope.js ADDED
@@ -0,0 +1,60 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ const REPO_MARKERS = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml"];
4
+ function parseAskTheWToml(filePath) {
5
+ if (!fs.existsSync(filePath)) {
6
+ return {};
7
+ }
8
+ const contents = fs.readFileSync(filePath, "utf8");
9
+ const readValue = (key) => {
10
+ const match = contents.match(new RegExp(`${key}\\s*=\\s*["']([^"']+)["']`, "i"));
11
+ return match?.[1]?.trim() || "";
12
+ };
13
+ return {
14
+ repoName: readValue("repo_name"),
15
+ repoRoot: readValue("repo_root"),
16
+ appPath: readValue("app_path"),
17
+ serviceName: readValue("service_name"),
18
+ functionalArea: readValue("functional_area"),
19
+ };
20
+ }
21
+ function findRepoRoot(startDirectory) {
22
+ let currentDirectory = path.resolve(startDirectory);
23
+ let highestMarkerDirectory = null;
24
+ while (true) {
25
+ if (fs.existsSync(path.join(currentDirectory, ".git"))) {
26
+ return currentDirectory;
27
+ }
28
+ if (REPO_MARKERS.some((marker) => fs.existsSync(path.join(currentDirectory, marker)))) {
29
+ highestMarkerDirectory = currentDirectory;
30
+ }
31
+ const parentDirectory = path.dirname(currentDirectory);
32
+ if (parentDirectory === currentDirectory) {
33
+ break;
34
+ }
35
+ currentDirectory = parentDirectory;
36
+ }
37
+ return highestMarkerDirectory;
38
+ }
39
+ export function resolvePluginScope(startDirectory = process.cwd()) {
40
+ const cwd = path.resolve(startDirectory);
41
+ const parsedConfig = parseAskTheWToml(path.join(cwd, ".asktheworld.toml"));
42
+ const repoRoot = parsedConfig.repoRoot || findRepoRoot(cwd) || cwd;
43
+ const repoName = parsedConfig.repoName || path.basename(repoRoot);
44
+ const relativePath = path.relative(repoRoot, cwd);
45
+ const normalizedRelativePath = relativePath && relativePath !== "." && !relativePath.startsWith("..")
46
+ ? relativePath.split(path.sep).join("/")
47
+ : "";
48
+ const appPath = parsedConfig.appPath || normalizedRelativePath || undefined;
49
+ return {
50
+ repoName,
51
+ repoRoot,
52
+ ...(appPath ? { appPath } : {}),
53
+ ...(parsedConfig.serviceName ? { serviceName: parsedConfig.serviceName } : {}),
54
+ };
55
+ }
56
+ export function resolveFunctionalAreaFromToml(startDirectory = process.cwd()) {
57
+ const cwd = path.resolve(startDirectory);
58
+ const parsedConfig = parseAskTheWToml(path.join(cwd, ".asktheworld.toml"));
59
+ return parsedConfig.functionalArea || "";
60
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@askthew/mcp-plugin",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "Ask The W MCP connector for capturing compact coding-agent session signals.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "askthew-mcp": "./dist/cli.js"
11
+ },
12
+ "files": [
13
+ "README.md",
14
+ "dist/cli.d.ts",
15
+ "dist/cli.js",
16
+ "dist/index.d.ts",
17
+ "dist/index.js",
18
+ "dist/install.d.ts",
19
+ "dist/install.js",
20
+ "dist/scope.d.ts",
21
+ "dist/scope.js"
22
+ ],
23
+ "keywords": [
24
+ "askthew",
25
+ "mcp",
26
+ "codex",
27
+ "claude-code",
28
+ "cursor",
29
+ "coding-agent"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./dist/index.js": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ },
43
+ "./cli": {
44
+ "types": "./dist/cli.d.ts",
45
+ "import": "./dist/cli.js"
46
+ },
47
+ "./install": {
48
+ "types": "./dist/install.d.ts",
49
+ "import": "./dist/install.js"
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "lint": "tsc --noEmit",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "npm run build && node --test dist/*.test.js"
57
+ },
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.3.0",
60
+ "zod": "^3.24.2"
61
+ },
62
+ "devDependencies": {
63
+ "typescript": "~5.8.2"
64
+ }
65
+ }