faf-mcp 2.1.2 → 2.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/CLAUDE.md +6 -4
  3. package/README.md +8 -6
  4. package/dist/src/cli.js +2 -15
  5. package/dist/src/cli.js.map +1 -1
  6. package/dist/src/faf-core/commands/bi-sync.js +3 -2
  7. package/dist/src/faf-core/commands/bi-sync.js.map +1 -1
  8. package/dist/src/faf-core/extract/turbocat-bridge.d.ts +34 -0
  9. package/dist/src/faf-core/extract/turbocat-bridge.js +84 -0
  10. package/dist/src/faf-core/extract/turbocat-bridge.js.map +1 -0
  11. package/dist/src/faf-core/inject.d.ts +19 -0
  12. package/dist/src/faf-core/inject.js +57 -0
  13. package/dist/src/faf-core/inject.js.map +1 -0
  14. package/dist/src/faf-core/parsers/agents-parser.js +3 -2
  15. package/dist/src/faf-core/parsers/agents-parser.js.map +1 -1
  16. package/dist/src/faf-core/parsers/cursorrules-parser.js +6 -2
  17. package/dist/src/faf-core/parsers/cursorrules-parser.js.map +1 -1
  18. package/dist/src/faf-core/parsers/gemini-parser.js +3 -2
  19. package/dist/src/faf-core/parsers/gemini-parser.js.map +1 -1
  20. package/dist/src/handlers/championship-tools.js +11 -11
  21. package/dist/src/handlers/championship-tools.js.map +1 -1
  22. package/dist/src/handlers/fileHandler.js +31 -22
  23. package/dist/src/handlers/fileHandler.js.map +1 -1
  24. package/dist/src/handlers/tools.d.ts +0 -4
  25. package/dist/src/handlers/tools.js +128 -372
  26. package/dist/src/handlers/tools.js.map +1 -1
  27. package/dist/src/server.d.ts +2 -4
  28. package/dist/src/server.js +1 -92
  29. package/dist/src/server.js.map +1 -1
  30. package/dist/src/utils/safe-path.d.ts +66 -0
  31. package/dist/src/utils/safe-path.js +203 -0
  32. package/dist/src/utils/safe-path.js.map +1 -0
  33. package/package.json +3 -7
  34. package/project.faf +4 -3
  35. package/scripts/check-stylesheet-drift.mjs +1 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * safe-path.ts — confinement for caller-supplied `path` arguments.
3
+ *
4
+ * Several MCP tools (`refresh_faf`, `faf_score`, `faf_get_orchestration_policy`,
5
+ * `refresh_blend`, …) accept a `path` argument and read a `.faf` context file
6
+ * from it. Historically that path flowed straight through `path.resolve()` into
7
+ * `fs.readFileSync()` with no confinement, so an absolute path or `../` traversal
8
+ * could read ANY file the server uid can read (CWE-22 / CWE-73 / CWE-200) and
9
+ * have its contents echoed back — e.g. `refresh_faf({path:"~/.ssh/id_rsa"})`.
10
+ *
11
+ * This module is the single chokepoint that closes that. Two layers:
12
+ *
13
+ * 1. Context-file allow-list (ALWAYS ON) — this server's only job is reading
14
+ * `.faf` / `.fafm` context files. When a caller path resolves to a *file*,
15
+ * it must be one of those. That alone blocks the entire secret-disclosure
16
+ * surface — `/etc/passwd`, `~/.ssh/id_rsa`, `~/.aws/credentials`, `.env`,
17
+ * etc. are none of them `.faf` files — regardless of directory. A `.faf`
18
+ * is a public project-context format, so reading one anywhere discloses no
19
+ * secrets (a planted one only echoes what the attacker already wrote).
20
+ *
21
+ * 2. Root confinement (OPT-IN) — when `FAF_ALLOWED_ROOTS` is set (OS-path
22
+ * delimited), the resolved path must additionally stay within one of those
23
+ * roots. Off by default so legitimate `.faf` files outside $HOME (CI temp
24
+ * fixtures, /opt, /srv, monorepos) keep working; operators who want a hard
25
+ * directory boundary opt in.
26
+ *
27
+ * Layer 1 is the security boundary; layer 2 is defense-in-depth for locked-down
28
+ * deployments. Either way, `..` traversal and absolute paths can never reach a
29
+ * non-context file.
30
+ */
31
+ export declare class PathConfinementError extends Error {
32
+ constructor(message: string);
33
+ }
34
+ /** True when `p`'s basename is a `.faf` / `.fafm` context file. */
35
+ export declare function isFafContextFile(p: string): boolean;
36
+ /** Opt-in allowed roots from `FAF_ALLOWED_ROOTS` (OS-delimited). Empty when
37
+ * unset → root confinement is not enforced (the `.faf`-only rule still is). */
38
+ export declare function allowedRoots(): string[];
39
+ /**
40
+ * Roots for the general-purpose file tools (`faf_read` / `faf_write`). Unlike
41
+ * the `.faf` tools, these legitimately handle any file *type* — but they must
42
+ * still be confined to the project. Default root = the process cwd; override /
43
+ * extend with `FAF_ALLOWED_ROOTS`.
44
+ */
45
+ export declare function fileOpRoots(): string[];
46
+ /**
47
+ * Confine a general-purpose file read/write path: any file type, but it must
48
+ * stay within fileOpRoots(). Closes absolute-path escapes (`~/.ssh/id_rsa`),
49
+ * `..` traversal, and arbitrary writes outside the project. Throws
50
+ * PathConfinementError on violation. Returns the safe (symlink-canonical) path.
51
+ */
52
+ export declare function confineFileOp(input: unknown): string;
53
+ export interface ConfineOptions {
54
+ /** Override allowed roots (resolved internally). Defaults to allowedRoots()
55
+ * (the opt-in `FAF_ALLOWED_ROOTS`). Empty = root confinement not enforced. */
56
+ roots?: string[];
57
+ /** When the resolved path is an existing file, require it be a `.faf`/`.fafm`
58
+ * context file. Use for sinks that read the path verbatim. Default true. */
59
+ requireFafFile?: boolean;
60
+ }
61
+ /**
62
+ * Resolve and confine a caller-supplied path. Returns the safe absolute path,
63
+ * or throws PathConfinementError. Never reaches the filesystem read itself —
64
+ * callers do that with the returned value.
65
+ */
66
+ export declare function confinePath(input: unknown, opts?: ConfineOptions): string;
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ /**
3
+ * safe-path.ts — confinement for caller-supplied `path` arguments.
4
+ *
5
+ * Several MCP tools (`refresh_faf`, `faf_score`, `faf_get_orchestration_policy`,
6
+ * `refresh_blend`, …) accept a `path` argument and read a `.faf` context file
7
+ * from it. Historically that path flowed straight through `path.resolve()` into
8
+ * `fs.readFileSync()` with no confinement, so an absolute path or `../` traversal
9
+ * could read ANY file the server uid can read (CWE-22 / CWE-73 / CWE-200) and
10
+ * have its contents echoed back — e.g. `refresh_faf({path:"~/.ssh/id_rsa"})`.
11
+ *
12
+ * This module is the single chokepoint that closes that. Two layers:
13
+ *
14
+ * 1. Context-file allow-list (ALWAYS ON) — this server's only job is reading
15
+ * `.faf` / `.fafm` context files. When a caller path resolves to a *file*,
16
+ * it must be one of those. That alone blocks the entire secret-disclosure
17
+ * surface — `/etc/passwd`, `~/.ssh/id_rsa`, `~/.aws/credentials`, `.env`,
18
+ * etc. are none of them `.faf` files — regardless of directory. A `.faf`
19
+ * is a public project-context format, so reading one anywhere discloses no
20
+ * secrets (a planted one only echoes what the attacker already wrote).
21
+ *
22
+ * 2. Root confinement (OPT-IN) — when `FAF_ALLOWED_ROOTS` is set (OS-path
23
+ * delimited), the resolved path must additionally stay within one of those
24
+ * roots. Off by default so legitimate `.faf` files outside $HOME (CI temp
25
+ * fixtures, /opt, /srv, monorepos) keep working; operators who want a hard
26
+ * directory boundary opt in.
27
+ *
28
+ * Layer 1 is the security boundary; layer 2 is defense-in-depth for locked-down
29
+ * deployments. Either way, `..` traversal and absolute paths can never reach a
30
+ * non-context file.
31
+ */
32
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ var desc = Object.getOwnPropertyDescriptor(m, k);
35
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
+ desc = { enumerable: true, get: function() { return m[k]; } };
37
+ }
38
+ Object.defineProperty(o, k2, desc);
39
+ }) : (function(o, m, k, k2) {
40
+ if (k2 === undefined) k2 = k;
41
+ o[k2] = m[k];
42
+ }));
43
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
44
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
45
+ }) : function(o, v) {
46
+ o["default"] = v;
47
+ });
48
+ var __importStar = (this && this.__importStar) || (function () {
49
+ var ownKeys = function(o) {
50
+ ownKeys = Object.getOwnPropertyNames || function (o) {
51
+ var ar = [];
52
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
53
+ return ar;
54
+ };
55
+ return ownKeys(o);
56
+ };
57
+ return function (mod) {
58
+ if (mod && mod.__esModule) return mod;
59
+ var result = {};
60
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
61
+ __setModuleDefault(result, mod);
62
+ return result;
63
+ };
64
+ })();
65
+ Object.defineProperty(exports, "__esModule", { value: true });
66
+ exports.PathConfinementError = void 0;
67
+ exports.isFafContextFile = isFafContextFile;
68
+ exports.allowedRoots = allowedRoots;
69
+ exports.fileOpRoots = fileOpRoots;
70
+ exports.confineFileOp = confineFileOp;
71
+ exports.confinePath = confinePath;
72
+ const path = __importStar(require("path"));
73
+ const os = __importStar(require("os"));
74
+ const fs = __importStar(require("fs"));
75
+ class PathConfinementError extends Error {
76
+ constructor(message) {
77
+ super(message);
78
+ this.name = 'PathConfinementError';
79
+ }
80
+ }
81
+ exports.PathConfinementError = PathConfinementError;
82
+ /** True when `p`'s basename is a `.faf` / `.fafm` context file. */
83
+ function isFafContextFile(p) {
84
+ const base = path.basename(p).toLowerCase();
85
+ return base === '.faf' || base.endsWith('.faf') || base.endsWith('.fafm');
86
+ }
87
+ /** Expand a leading `~` / `~/` for the CURRENT user only. `~otheruser` is left
88
+ * literal (it will then fail the root check rather than reaching another home). */
89
+ function expandTilde(p) {
90
+ if (p === '~')
91
+ return os.homedir();
92
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
93
+ return path.join(os.homedir(), p.slice(2));
94
+ }
95
+ return p;
96
+ }
97
+ /** Opt-in allowed roots from `FAF_ALLOWED_ROOTS` (OS-delimited). Empty when
98
+ * unset → root confinement is not enforced (the `.faf`-only rule still is). */
99
+ function allowedRoots() {
100
+ const env = process.env.FAF_ALLOWED_ROOTS;
101
+ if (env && env.trim()) {
102
+ return env
103
+ .split(path.delimiter)
104
+ .map((r) => r.trim())
105
+ .filter(Boolean)
106
+ .map((r) => path.resolve(expandTilde(r)));
107
+ }
108
+ return [];
109
+ }
110
+ function withinRoots(resolved, roots) {
111
+ return roots.some((root) => resolved === root || resolved.startsWith(root + path.sep));
112
+ }
113
+ /**
114
+ * Symlink-canonical absolute path, tolerant of a not-yet-existing target
115
+ * (a `faf_write` to a new file). Resolves the nearest EXISTING ancestor through
116
+ * symlinks, then re-appends the missing tail — so a new file under /tmp matches
117
+ * a /private/tmp root on macOS instead of slipping past the confinement check.
118
+ */
119
+ function canonicalize(input) {
120
+ let cur = path.resolve(input);
121
+ const tail = [];
122
+ for (;;) {
123
+ try {
124
+ const real = fs.realpathSync(cur);
125
+ return tail.length ? path.join(real, ...tail.reverse()) : real;
126
+ }
127
+ catch {
128
+ const parent = path.dirname(cur);
129
+ if (parent === cur)
130
+ return path.resolve(input); // hit filesystem root
131
+ tail.push(path.basename(cur));
132
+ cur = parent;
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * Roots for the general-purpose file tools (`faf_read` / `faf_write`). Unlike
138
+ * the `.faf` tools, these legitimately handle any file *type* — but they must
139
+ * still be confined to the project. Default root = the process cwd; override /
140
+ * extend with `FAF_ALLOWED_ROOTS`.
141
+ */
142
+ function fileOpRoots() {
143
+ const opt = allowedRoots();
144
+ if (opt.length)
145
+ return opt;
146
+ // Default: the project (cwd) plus the OS temp dir(s) — legitimate scratch
147
+ // space for tools. Still blocks the high-value targets — $HOME secrets
148
+ // (~/.ssh, ~/.aws), /etc, and anything reached via ../ traversal.
149
+ const roots = [path.resolve(process.cwd()), os.tmpdir()];
150
+ // On macOS the canonical system temp (/tmp → /private/tmp) differs from
151
+ // os.tmpdir() (/var/folders/...); include it (roots are canonicalized later).
152
+ if (process.platform !== 'win32')
153
+ roots.push('/tmp');
154
+ return roots;
155
+ }
156
+ /**
157
+ * Confine a general-purpose file read/write path: any file type, but it must
158
+ * stay within fileOpRoots(). Closes absolute-path escapes (`~/.ssh/id_rsa`),
159
+ * `..` traversal, and arbitrary writes outside the project. Throws
160
+ * PathConfinementError on violation. Returns the safe (symlink-canonical) path.
161
+ */
162
+ function confineFileOp(input) {
163
+ return confinePath(input, { requireFafFile: false, roots: fileOpRoots() });
164
+ }
165
+ /**
166
+ * Resolve and confine a caller-supplied path. Returns the safe absolute path,
167
+ * or throws PathConfinementError. Never reaches the filesystem read itself —
168
+ * callers do that with the returned value.
169
+ */
170
+ function confinePath(input, opts = {}) {
171
+ if (typeof input !== 'string' || input.length === 0) {
172
+ throw new PathConfinementError('path must be a non-empty string');
173
+ }
174
+ if (input.includes('\0')) {
175
+ throw new PathConfinementError('path contains a null byte');
176
+ }
177
+ // Canonicalize THROUGH symlinks when the target exists. This closes the
178
+ // symlink bypass: a file named `project.faf` that is actually a symlink to
179
+ // `/etc/passwd` would pass a lexical name check but read the secret. We check
180
+ // the real target's name instead. Missing paths stay lexical (nothing to read
181
+ // yet — a directory walk or a downstream ENOENT handles them).
182
+ const resolved = canonicalize(expandTilde(input));
183
+ let isFile = false;
184
+ try {
185
+ isFile = fs.statSync(resolved).isFile();
186
+ }
187
+ catch {
188
+ isFile = false;
189
+ }
190
+ const roots = (opts.roots ?? allowedRoots()).map(canonicalize);
191
+ // Layer 2 (opt-in): enforce root confinement only when roots are configured.
192
+ if (roots.length > 0 && !withinRoots(resolved, roots)) {
193
+ throw new PathConfinementError(`path escapes FAF_ALLOWED_ROOTS: "${input}".`);
194
+ }
195
+ // Layer 1 (always on): a resolved *file* must be a `.faf`/`.fafm` context file.
196
+ const requireFaf = opts.requireFafFile ?? true;
197
+ if (requireFaf && isFile && !isFafContextFile(resolved)) {
198
+ throw new PathConfinementError(`refusing to read a non-context file via \`path\`: "${input}". ` +
199
+ `Only .faf / .fafm files (or a directory containing one) are allowed.`);
200
+ }
201
+ return resolved;
202
+ }
203
+ //# sourceMappingURL=safe-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../../../src/utils/safe-path.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "faf-mcp",
3
- "version": "2.1.2",
4
- "mcpName": "io.github.Wolfe-Jam/faf-mcp",
3
+ "version": "2.2.0",
4
+ "mcpName": "one.faf/faf-mcp",
5
5
  "description": "Persistent Project Context for Cursor, IDEs and VS Code. IANA-registered .faf format, 25 MCP tools, 309 tests.",
6
6
  "icon": "./assets/icons/faf-icon-256.png",
7
7
  "logo": "./assets/icons/faf-icon-256.png",
@@ -102,15 +102,11 @@
102
102
  },
103
103
  "dependencies": {
104
104
  "@modelcontextprotocol/sdk": "^1.27.1",
105
- "cors": "^2.8.5",
106
- "express": "^4.21.0",
107
- "faf-cli": "^6.7.1",
105
+ "faf-cli": "^6.10.1",
108
106
  "faf-scoring-kernel": "^2.0.3",
109
107
  "yaml": "^2.4.1"
110
108
  },
111
109
  "devDependencies": {
112
- "@types/cors": "^2.8.19",
113
- "@types/express": "^5.0.3",
114
110
  "@types/jsonwebtoken": "^9.0.5",
115
111
  "@types/node": "^20.11.0",
116
112
  "@typescript-eslint/eslint-plugin": "^7.0.0",
package/project.faf CHANGED
@@ -5,17 +5,18 @@ project:
5
5
  main_language: TypeScript
6
6
  type: mcp
7
7
  framework: MCP SDK (TS)
8
+ homepage: https://faf.one
8
9
  stack:
9
10
  frontend: slotignored
10
11
  css_framework: slotignored
11
12
  ui_library: slotignored
12
13
  state_management: slotignored
13
14
  backend: MCP SDK (TS)
14
- api_type: MCP (stdio/HTTP-SSE)
15
+ api_type: MCP (stdio + Streamable HTTP)
15
16
  runtime: Node.js >=18
16
17
  database: slotignored
17
18
  connection: slotignored
18
- hosting: Vercel
19
+ hosting: npm + Cloudflare edge
19
20
  build: TypeScript (tsc)
20
21
  cicd: GitHub Actions
21
22
  monorepo_tool: slotignored
@@ -29,7 +30,7 @@ human_context:
29
30
  who: Developers using Claude, Cursor, Windsurf, VS Code, Cline, and any MCP-compatible IDE
30
31
  what: Universal MCP server providing .faf context tools — 25 core + 36 advanced, interop with AGENTS.md, .cursorrules, GEMINI.md
31
32
  why: Eliminate the 20-minute AI context tax — give AI instant project understanding in 30 seconds
32
- where: Vercel web (faf-mcp.vercel.app) · npm · MCP Registry · any MCP-compatible IDE — people get it how they wish
33
+ where: npm · MCP Registry · Cloudflare edge (ide.faf.one/mcp/v1) · any MCP-compatible IDE — people get it how they wish
33
34
  when: Production/Stable — v2.0.0 WJTTC certified (309/309 tests, 9 suites)
34
35
  how: npx faf-mcp or npm install -g faf-mcp, then add to your MCP config
35
36
  monorepo:
@@ -21,7 +21,7 @@ import { dirname, join } from 'node:path';
21
21
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
22
22
 
23
23
  const SOURCE = 'docs/style-source.html';
24
- const SURFACES = ['docs/index.html', 'api/index.ts'];
24
+ const SURFACES = ['docs/index.html'];
25
25
 
26
26
  const OPEN = '/* === faf-mcp:stylesheet canonical';
27
27
  const CLOSE = '/* === /faf-mcp:stylesheet canonical === */';