opencontext-mcp 0.1.0 → 1.1.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
@@ -3,6 +3,7 @@
3
3
  [![MCP](https://img.shields.io/badge/MCP-compatible-2563eb)](https://modelcontextprotocol.io/)
4
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org/)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933)](https://nodejs.org/)
6
+ [![Website](https://img.shields.io/badge/URL-opencntx.dev-c42f52?link=https%3A%2F%2Fopencntx.dev)](https://opencntx.dev/)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
8
 
8
9
  OpenContext MCP gives AI agents persistent, project-local memory.
@@ -15,6 +16,9 @@ The result is simple and transparent: your agent gets memory, and you keep full
15
16
 
16
17
  - `save_context`: save markdown context to `.opencontext/<topic>.md`
17
18
  - `read_context`: read one saved topic or list all available topics
19
+ - **Auto-Index**: automatic `index.md` generation with topic metadata
20
+ - **Write Guard**: input validation, size limits, and prompt injection protection
21
+ - **Config System**: customizable via `.opencontext.jsonc` or `.opencontext.json`
18
22
  - Project-specific storage based on the client's current working directory
19
23
  - No database, account, cloud sync, or hidden state
20
24
  - Works with MCP-compatible clients over stdio
@@ -54,9 +58,9 @@ Add OpenContext MCP to your OpenCode MCP configuration:
54
58
  }
55
59
  ```
56
60
 
57
- ### Cursor
61
+ ### Cursor / Claude Desktop
58
62
 
59
- Add this server to your Cursor MCP configuration:
63
+ Cursor and Claude Desktop use the same MCP server configuration:
60
64
 
61
65
  ```json
62
66
  {
@@ -69,36 +73,52 @@ Add this server to your Cursor MCP configuration:
69
73
  }
70
74
  ```
71
75
 
72
- ### Claude Desktop
76
+ Restart your MCP client after updating the configuration.
73
77
 
74
- Add this entry to your Claude Desktop MCP configuration:
78
+ ## Configuration
75
79
 
76
- ```json
80
+ OpenContext can be customized with a config file in your project root. Create `.opencontext.jsonc` or `.opencontext.json`:
81
+
82
+ ```jsonc
77
83
  {
78
- "mcpServers": {
79
- "opencontext": {
80
- "command": "npx",
81
- "args": ["-y", "opencontext-mcp"]
82
- }
84
+ // Custom storage path (default: ".opencontext")
85
+ "path": ".opencontext",
86
+
87
+ // Disable write operations
88
+ "readOnly": false,
89
+
90
+ // Pause all tool access
91
+ "disabled": false,
92
+
93
+ // Auto-generate index.md with topic metadata
94
+ "autoIndex": true,
95
+
96
+ // Write guard settings
97
+ "guard": {
98
+ "enabled": true,
99
+ "maxFileSizeKb": 50,
100
+ "strictPatternCheck": true
101
+ },
102
+
103
+ // History backup settings (coming soon)
104
+ "history": {
105
+ "enabled": false,
106
+ "maxBackupsPerTopic": 5,
107
+ "retentionDays": 7
83
108
  }
84
109
  }
85
110
  ```
86
111
 
87
- Restart your MCP client after updating configuration.
88
-
89
- ## How It Works
112
+ ### Guard System
90
113
 
91
- OpenContext MCP stores files relative to the process working directory used by your MCP client:
92
-
93
- ```text
94
- your-project/
95
- .opencontext/
96
- architecture.md
97
- api-contracts.md
98
- coding_rules.md
99
- ```
114
+ The write guard protects against:
100
115
 
101
- Topics must be lowercase snake_case or kebab-case, such as `architecture`, `api-contracts`, or `coding_rules`.
116
+ - **Empty content**: prevents saving blank or whitespace-only files
117
+ - **Payload too large**: configurable max file size (default 50KB)
118
+ - **Invalid topics**: enforces snake_case or kebab-case naming
119
+ - **Path traversal**: blocks `..`, absolute paths, and directory escapes
120
+ - **Reserved topics**: prevents overwriting system files like `index.md`
121
+ - **Prompt injection**: detects and blocks common injection patterns
102
122
 
103
123
  ## Available Tools
104
124
 
@@ -154,10 +174,7 @@ Recommended workflow:
154
174
  3. Let agents update context when they discover something that should survive the current chat.
155
175
  4. Review `.opencontext/` files like normal project documentation.
156
176
 
157
- Copy-ready agent prompts:
158
-
159
- - [`examples/plan-agent.md`](examples/plan-agent.md) -- architect that analyzes requirements and saves context
160
- - [`examples/build-agent.md`](examples/build-agent.md) -- developer that reads context before coding
177
+ For pre-built agent prompts, see the [documentation](https://opencntx.dev/docs/agents).
161
178
 
162
179
  ## Version Control
163
180
 
@@ -175,33 +192,45 @@ Or ignore it when context should stay local:
175
192
 
176
193
  Because files are plain markdown, both approaches are safe and easy to audit.
177
194
 
195
+ ## Project Structure
196
+
197
+ ```text
198
+ opencontext/
199
+ ├── packages/
200
+ │ └── opencontext/ # MCP server package
201
+ │ ├── src/
202
+ │ │ ├── index.ts # CLI entrypoint (stdio transport)
203
+ │ │ ├── server.ts # McpServer factory and tool registration
204
+ │ │ ├── config.ts # Config loading (.opencontext.jsonc)
205
+ │ │ ├── context-store.ts # Filesystem operations for .opencontext/
206
+ │ │ ├── validation.ts # Topic validation and write guard
207
+ │ │ └── types.ts # Shared constants and error helpers
208
+ │ └── test/
209
+ │ ├── context-store.test.ts
210
+ │ ├── validation.test.ts
211
+ │ ├── guard.test.ts
212
+ │ └── auto-index.test.ts
213
+ ├── apps/
214
+ │ └── web/ # Website (Next.js)
215
+ │ └── src/
216
+ │ ├── app/
217
+ │ │ ├── page.tsx # Landing page
218
+ │ │ ├── docs/ # Documentation
219
+ │ │ ├── imprint/
220
+ │ │ └── privacy-policy/
221
+ │ └── components/
222
+ ├── CONTRIBUTING.md
223
+ ├── SECURITY.md
224
+ └── README.md
225
+ ```
226
+
178
227
  ## Development
179
228
 
180
229
  ```bash
181
230
  pnpm install
182
- pnpm build # compile TypeScript
231
+ pnpm build # compile all packages
183
232
  pnpm typecheck # type-check without emitting
184
233
  pnpm test # run vitest suite
185
- pnpm start # start the MCP server
186
- ```
187
-
188
- Project structure:
189
-
190
- ```text
191
- src/
192
- index.ts CLI entrypoint (stdio transport, startup error handling)
193
- server.ts McpServer factory and tool registration
194
- context-store.ts Filesystem operations for .opencontext/
195
- validation.ts Topic validation and input rules
196
- types.ts Shared constants, error classes, and result helpers
197
- test/
198
- validation.test.ts Unit tests for topic validation
199
- context-store.test.ts Unit tests for read/write/list operations
200
- examples/
201
- plan-agent.md Architect system prompt
202
- build-agent.md Developer system prompt
203
- README.md User documentation
204
- LICENSE MIT license
205
234
  ```
206
235
 
207
236
  ## License
@@ -0,0 +1,24 @@
1
+ export interface OpenContextConfig {
2
+ $schema?: string;
3
+ path?: string;
4
+ readOnly?: boolean;
5
+ disabled?: boolean;
6
+ autoIndex?: boolean;
7
+ history?: {
8
+ enabled?: boolean;
9
+ maxBackupsPerTopic?: number;
10
+ retentionDays?: number;
11
+ };
12
+ guard?: {
13
+ enabled?: boolean;
14
+ maxFileSizeKb?: number;
15
+ strictPatternCheck?: boolean;
16
+ };
17
+ }
18
+ export type ResolvedConfig = Required<Omit<OpenContextConfig, "$schema">>;
19
+ export declare const DEFAULT_CONFIG: ResolvedConfig;
20
+ /**
21
+ * Loads configuration from .opencontext.jsonc or .opencontext.json in the given directory.
22
+ * Falls back to DEFAULT_CONFIG if no file exists or parsing fails.
23
+ */
24
+ export declare function loadConfig(cwd?: string): Promise<ResolvedConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,114 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ export const DEFAULT_CONFIG = {
4
+ path: ".opencontext",
5
+ readOnly: false,
6
+ disabled: false,
7
+ autoIndex: true,
8
+ history: {
9
+ enabled: false,
10
+ maxBackupsPerTopic: 5,
11
+ retentionDays: 7,
12
+ },
13
+ guard: {
14
+ enabled: true,
15
+ maxFileSizeKb: 50,
16
+ strictPatternCheck: true,
17
+ },
18
+ };
19
+ const CONFIG_CANDIDATES = [".opencontext.jsonc", ".opencontext.json"];
20
+ /**
21
+ * Strips JSONC content into parseable JSON by removing comments and trailing commas.
22
+ * Handles single-line (//), multi-line (/* * /), and trailing commas.
23
+ */
24
+ function stripJsonComments(raw) {
25
+ let result = "";
26
+ let i = 0;
27
+ const len = raw.length;
28
+ while (i < len) {
29
+ const ch = raw[i];
30
+ // Strings: copy verbatim (including escaped characters)
31
+ if (ch === '"') {
32
+ result += ch;
33
+ i++;
34
+ while (i < len && raw[i] !== '"') {
35
+ if (raw[i] === "\\") {
36
+ result += raw[i];
37
+ i++;
38
+ }
39
+ if (i < len) {
40
+ result += raw[i];
41
+ i++;
42
+ }
43
+ }
44
+ if (i < len) {
45
+ result += raw[i]; // closing quote
46
+ i++;
47
+ }
48
+ continue;
49
+ }
50
+ // Single-line comment: skip until newline
51
+ if (ch === "/" && i + 1 < len && raw[i + 1] === "/") {
52
+ while (i < len && raw[i] !== "\n") {
53
+ i++;
54
+ }
55
+ continue;
56
+ }
57
+ // Multi-line comment: skip until */
58
+ if (ch === "/" && i + 1 < len && raw[i + 1] === "*") {
59
+ i += 2;
60
+ while (i < len - 1 && !(raw[i] === "*" && raw[i + 1] === "/")) {
61
+ i++;
62
+ }
63
+ i += 2; // skip */
64
+ continue;
65
+ }
66
+ result += ch;
67
+ i++;
68
+ }
69
+ // Strip trailing commas before closing braces/brackets
70
+ return result.replace(/,(\s*[}\]])/g, "$1");
71
+ }
72
+ function deepMerge(target, source) {
73
+ const result = { ...target };
74
+ for (const key of Object.keys(source)) {
75
+ const sourceVal = source[key];
76
+ const targetVal = result[key];
77
+ if (sourceVal !== null &&
78
+ typeof sourceVal === "object" &&
79
+ !Array.isArray(sourceVal) &&
80
+ targetVal !== null &&
81
+ typeof targetVal === "object" &&
82
+ !Array.isArray(targetVal)) {
83
+ result[key] = deepMerge(targetVal, sourceVal);
84
+ }
85
+ else if (sourceVal !== undefined) {
86
+ result[key] = sourceVal;
87
+ }
88
+ }
89
+ return result;
90
+ }
91
+ /**
92
+ * Loads configuration from .opencontext.jsonc or .opencontext.json in the given directory.
93
+ * Falls back to DEFAULT_CONFIG if no file exists or parsing fails.
94
+ */
95
+ export async function loadConfig(cwd = process.cwd()) {
96
+ for (const filename of CONFIG_CANDIDATES) {
97
+ const filePath = path.join(cwd, filename);
98
+ try {
99
+ const raw = await readFile(filePath, "utf8");
100
+ const cleaned = stripJsonComments(raw);
101
+ const parsed = JSON.parse(cleaned);
102
+ return deepMerge(DEFAULT_CONFIG, parsed);
103
+ }
104
+ catch (error) {
105
+ if (error.code === "ENOENT") {
106
+ continue; // file doesn't exist, try next candidate
107
+ }
108
+ console.error(`OpenContext: failed to parse ${filename}: ${error.message}`);
109
+ return DEFAULT_CONFIG;
110
+ }
111
+ }
112
+ return DEFAULT_CONFIG;
113
+ }
114
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,50 @@
1
+ import { type ResolvedConfig } from "./config.js";
2
+ /**
3
+ * Manages context file storage operations.
4
+ * Handles reading, writing, and listing context files in the configured directory.
5
+ */
1
6
  export declare class ContextStore {
2
7
  private readonly basePath;
3
- constructor(basePath?: string);
8
+ private readonly config;
9
+ private readonly contextDir;
10
+ constructor(basePath?: string, config?: ResolvedConfig);
11
+ /** Returns the absolute path to the context directory. */
4
12
  getContextDirectory(): string;
13
+ /** Returns the absolute path to a topic file. */
5
14
  getTopicFilePath(topic: string): string;
15
+ /**
16
+ * Saves context content to a topic file.
17
+ * Validates payload using WriteGuard before writing.
18
+ * @param topicInput - Topic name (will be validated and trimmed)
19
+ * @param content - Markdown content to save
20
+ * @returns Success message with file location
21
+ * @throws UserInputError if validation fails
22
+ */
6
23
  saveContext(topicInput: string, content: string): Promise<string>;
24
+ /**
25
+ * Reads context content from a topic file, or lists all topics if none specified.
26
+ * @param topicInput - Optional topic name to read
27
+ * @returns Topic content or list of available topics
28
+ * @throws UserInputError if topic doesn't exist or is invalid
29
+ */
7
30
  readContext(topicInput?: string): Promise<string>;
31
+ /**
32
+ * Lists all available context topics.
33
+ * Scans the context directory for .md files and returns sorted topic names.
34
+ * @returns Sorted array of topic names
35
+ */
8
36
  listTopics(): Promise<string[]>;
37
+ /**
38
+ * Extracts a short description from markdown content.
39
+ * Strategy: frontmatter description → first heading → first paragraph → fallback.
40
+ * @param content - Raw markdown content
41
+ * @returns Truncated description (max 120 chars)
42
+ */
43
+ private extractDescription;
44
+ /**
45
+ * Rebuilds the auto-generated index.md file in the context directory.
46
+ * Scans all topic files, extracts metadata, and writes a compact index.
47
+ * @returns The generated index markdown content
48
+ */
49
+ rebuildContextIndex(): Promise<string>;
9
50
  }
@@ -1,48 +1,86 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { CONTEXT_DIRECTORY_NAME, UserInputError, isNodeError } from "./types.js";
4
- import { validateTopic } from "./validation.js";
1
+ import { mkdir, readdir, readFile, writeFile, stat } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { INDEX_FILENAME, UserInputError, isNodeError } from "./types.js";
4
+ import { validateTopic, validateWritePayload, sanitizeTopicPath } from "./validation.js";
5
+ import { DEFAULT_CONFIG } from "./config.js";
6
+ /**
7
+ * Manages context file storage operations.
8
+ * Handles reading, writing, and listing context files in the configured directory.
9
+ */
5
10
  export class ContextStore {
6
11
  basePath;
7
- constructor(basePath = process.cwd()) {
12
+ config;
13
+ contextDir;
14
+ constructor(basePath = process.cwd(), config = DEFAULT_CONFIG) {
8
15
  this.basePath = basePath;
16
+ this.config = config;
17
+ this.contextDir = path.join(this.basePath, this.config.path);
9
18
  }
19
+ /** Returns the absolute path to the context directory. */
10
20
  getContextDirectory() {
11
- return path.join(this.basePath, CONTEXT_DIRECTORY_NAME);
21
+ return this.contextDir;
12
22
  }
23
+ /** Returns the absolute path to a topic file. */
13
24
  getTopicFilePath(topic) {
14
- return path.join(this.getContextDirectory(), `${topic}.md`);
25
+ return path.join(this.contextDir, `${topic}.md`);
15
26
  }
27
+ /**
28
+ * Saves context content to a topic file.
29
+ * Validates payload using WriteGuard before writing.
30
+ * @param topicInput - Topic name (will be validated and trimmed)
31
+ * @param content - Markdown content to save
32
+ * @returns Success message with file location
33
+ * @throws UserInputError if validation fails
34
+ */
16
35
  async saveContext(topicInput, content) {
36
+ const guardResult = validateWritePayload(topicInput, content, {
37
+ maxFileSizeKb: this.config.guard.maxFileSizeKb,
38
+ strictPatternCheck: this.config.guard.strictPatternCheck,
39
+ });
40
+ if (!guardResult.allowed) {
41
+ console.error(`WriteGuard Rejected: ${guardResult.reason} (Code: ${guardResult.code})`);
42
+ throw new UserInputError(`WriteGuard Rejected: ${guardResult.reason} (Code: ${guardResult.code})`);
43
+ }
17
44
  const topic = validateTopic(topicInput);
18
- const contextDirectory = this.getContextDirectory();
19
- const filePath = this.getTopicFilePath(topic);
20
- await mkdir(contextDirectory, { recursive: true });
45
+ const filePath = sanitizeTopicPath(this.contextDir, topic);
46
+ await mkdir(this.contextDir, { recursive: true });
21
47
  await writeFile(filePath, content, "utf8");
22
- return `Saved context topic "${topic}" to ${CONTEXT_DIRECTORY_NAME}/${topic}.md.`;
48
+ return `Saved context topic "${topic}" to ${this.config.path}/${topic}.md.`;
23
49
  }
50
+ /**
51
+ * Reads context content from a topic file, or lists all topics if none specified.
52
+ * @param topicInput - Optional topic name to read
53
+ * @returns Topic content or list of available topics
54
+ * @throws UserInputError if topic doesn't exist or is invalid
55
+ */
24
56
  async readContext(topicInput) {
25
57
  if (topicInput !== undefined) {
26
58
  const topic = validateTopic(topicInput);
27
59
  try {
28
- return await readFile(this.getTopicFilePath(topic), "utf8");
60
+ const filePath = sanitizeTopicPath(this.contextDir, topic);
61
+ return await readFile(filePath, "utf8");
29
62
  }
30
63
  catch (error) {
31
64
  if (isNodeError(error) && error.code === "ENOENT") {
32
- throw new UserInputError(`No context found for topic "${topic}" at ${CONTEXT_DIRECTORY_NAME}/${topic}.md.`);
65
+ throw new UserInputError(`No context found for topic "${topic}" at ${this.config.path}/${topic}.md.`);
33
66
  }
34
67
  throw error;
35
68
  }
36
69
  }
37
70
  const topics = await this.listTopics();
38
71
  if (topics.length === 0) {
39
- return `No OpenContext topics found in ${CONTEXT_DIRECTORY_NAME}/. Use save_context to create one.`;
72
+ return `No OpenContext topics found in ${this.config.path}/. Use save_context to create one.`;
40
73
  }
41
- return `Available OpenContext topics:\n\n${topics.map((topic) => `- ${topic}`).join("\n")}`;
74
+ return this.rebuildContextIndex();
42
75
  }
76
+ /**
77
+ * Lists all available context topics.
78
+ * Scans the context directory for .md files and returns sorted topic names.
79
+ * @returns Sorted array of topic names
80
+ */
43
81
  async listTopics() {
44
82
  try {
45
- const entries = await readdir(this.getContextDirectory(), { withFileTypes: true });
83
+ const entries = await readdir(this.contextDir, { withFileTypes: true });
46
84
  return entries
47
85
  .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
48
86
  .map((entry) => entry.name.slice(0, -".md".length))
@@ -55,5 +93,103 @@ export class ContextStore {
55
93
  throw error;
56
94
  }
57
95
  }
96
+ /**
97
+ * Extracts a short description from markdown content.
98
+ * Strategy: frontmatter description → first heading → first paragraph → fallback.
99
+ * @param content - Raw markdown content
100
+ * @returns Truncated description (max 120 chars)
101
+ */
102
+ extractDescription(content) {
103
+ const trimmed = content.trim();
104
+ if (!trimmed) {
105
+ return "No summary available.";
106
+ }
107
+ // Strategy A: YAML frontmatter with description key
108
+ if (trimmed.startsWith("---")) {
109
+ const endIdx = trimmed.indexOf("---", 3);
110
+ if (endIdx !== -1) {
111
+ const frontmatter = trimmed.slice(3, endIdx);
112
+ const descMatch = frontmatter.match(/description:\s*["']?(.+?)["']?\s*$/m);
113
+ if (descMatch?.[1]) {
114
+ return descMatch[1].slice(0, 120);
115
+ }
116
+ }
117
+ }
118
+ const lines = trimmed.split("\n");
119
+ // Strategy B: First heading
120
+ for (const line of lines) {
121
+ const headingMatch = line.match(/^#\s+(.+)$/);
122
+ if (headingMatch?.[1]) {
123
+ return headingMatch[1].slice(0, 120);
124
+ }
125
+ }
126
+ // Strategy C: First non-empty paragraph line
127
+ for (const line of lines) {
128
+ const stripped = line.trim();
129
+ if (stripped && !stripped.startsWith("#")) {
130
+ return stripped.length > 120 ? stripped.slice(0, 120) + "..." : stripped;
131
+ }
132
+ }
133
+ return "No summary available.";
134
+ }
135
+ /**
136
+ * Rebuilds the auto-generated index.md file in the context directory.
137
+ * Scans all topic files, extracts metadata, and writes a compact index.
138
+ * @returns The generated index markdown content
139
+ */
140
+ async rebuildContextIndex() {
141
+ let entries;
142
+ try {
143
+ entries = await readdir(this.contextDir, { withFileTypes: true });
144
+ }
145
+ catch (error) {
146
+ if (isNodeError(error) && error.code === "ENOENT") {
147
+ return "";
148
+ }
149
+ throw error;
150
+ }
151
+ const topicFiles = entries
152
+ .filter((entry) => entry.isFile() &&
153
+ entry.name.endsWith(".md") &&
154
+ entry.name !== INDEX_FILENAME &&
155
+ entry.name !== "README.md")
156
+ .sort((a, b) => a.name.localeCompare(b.name));
157
+ if (topicFiles.length === 0) {
158
+ return "";
159
+ }
160
+ const topicEntries = [];
161
+ for (const entry of topicFiles) {
162
+ const filePath = path.join(this.contextDir, entry.name);
163
+ const content = await readFile(filePath, "utf8");
164
+ const fileStat = await stat(filePath);
165
+ const topic = entry.name.slice(0, -".md".length);
166
+ const description = this.extractDescription(content);
167
+ const date = fileStat.mtime.toISOString().slice(0, 10);
168
+ const sizeBytes = Buffer.byteLength(content, "utf8");
169
+ topicEntries.push({ topic, filename: entry.name, description, date, sizeBytes });
170
+ }
171
+ const lines = [
172
+ "# Project Context Index",
173
+ "<!-- AUTO-GENERATED BY OPENCONTEXT - DO NOT EDIT MANUALLY -->",
174
+ "",
175
+ "Available context topics in this project:",
176
+ "",
177
+ ];
178
+ for (const t of topicEntries) {
179
+ const sizeStr = t.sizeBytes >= 1024
180
+ ? `${(t.sizeBytes / 1024).toFixed(1)} KB`
181
+ : `${t.sizeBytes} B`;
182
+ lines.push(`- **${t.topic}** (\`${t.filename}\`) - Updated: ${t.date} (${sizeStr})`);
183
+ lines.push(` > ${t.description}`);
184
+ }
185
+ lines.push("");
186
+ lines.push("---");
187
+ lines.push("*To read a specific context topic, call `read_context` with the topic name.*");
188
+ lines.push("");
189
+ const indexContent = lines.join("\n");
190
+ const indexPath = path.join(this.contextDir, INDEX_FILENAME);
191
+ await writeFile(indexPath, indexContent, "utf8");
192
+ return indexContent;
193
+ }
58
194
  }
59
195
  //# sourceMappingURL=context-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-store.js","sourceRoot":"","sources":["../src/context-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-store.js","sourceRoot":"","sources":["../src/context-store.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,8 +2,12 @@
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { createOpenContextServer } from "./server.js";
4
4
  import { SERVER_NAME, getErrorMessage } from "./types.js";
5
+ /**
6
+ * Main entry point for the OpenContext MCP server.
7
+ * Creates server instance and connects to stdio transport.
8
+ */
5
9
  async function main() {
6
- const server = createOpenContextServer();
10
+ const server = await createOpenContextServer();
7
11
  const transport = new StdioServerTransport();
8
12
  await server.connect(transport);
9
13
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1D,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,uBAAuB,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,mBAAmB,WAAW,KAAK,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1D;;;GAGG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,MAAM,uBAAuB,EAAE,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,mBAAmB,WAAW,KAAK,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/server.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function createOpenContextServer(basePath?: string): McpServer;
2
+ import { type ResolvedConfig } from "./config.js";
3
+ /**
4
+ * Creates and configures the OpenContext MCP server.
5
+ * Registers all available tools (save_context, read_context).
6
+ * @param basePath - Optional base directory (defaults to cwd)
7
+ * @param config - Optional pre-loaded config (loads from disk if omitted)
8
+ */
9
+ export declare function createOpenContextServer(basePath?: string, config?: ResolvedConfig): Promise<McpServer>;
package/dist/server.js CHANGED
@@ -2,15 +2,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { ContextStore } from "./context-store.js";
4
4
  import { SERVER_NAME, SERVER_VERSION, getErrorMessage, textResult } from "./types.js";
5
- export function createOpenContextServer(basePath) {
6
- const store = new ContextStore(basePath);
5
+ import { loadConfig } from "./config.js";
6
+ /**
7
+ * Creates and configures the OpenContext MCP server.
8
+ * Registers all available tools (save_context, read_context).
9
+ * @param basePath - Optional base directory (defaults to cwd)
10
+ * @param config - Optional pre-loaded config (loads from disk if omitted)
11
+ */
12
+ export async function createOpenContextServer(basePath, config) {
13
+ const resolvedConfig = config ?? await loadConfig(basePath);
14
+ const store = new ContextStore(basePath, resolvedConfig);
7
15
  const server = new McpServer({
8
16
  name: SERVER_NAME,
9
17
  version: SERVER_VERSION,
10
18
  });
11
19
  server.registerTool("save_context", {
12
20
  title: "Save Context",
13
- description: "Persist markdown project context, architectural rules, or decisions into .opencontext/<topic>.md in the current working directory.",
21
+ description: `Persist markdown project context, architectural rules, or decisions into ${resolvedConfig.path}/<topic>.md in the current working directory.`,
14
22
  inputSchema: {
15
23
  topic: z
16
24
  .string()
@@ -19,6 +27,12 @@ export function createOpenContextServer(basePath) {
19
27
  content: z.string().min(1).describe("Markdown content to save for this project topic."),
20
28
  },
21
29
  }, async ({ topic, content }) => {
30
+ if (resolvedConfig.disabled) {
31
+ return textResult("OpenContext is currently paused. Tool access is disabled via configuration.");
32
+ }
33
+ if (resolvedConfig.readOnly) {
34
+ return textResult("OpenContext is in read-only mode. Write operations are disabled via configuration.", true);
35
+ }
22
36
  try {
23
37
  const result = await store.saveContext(topic, content);
24
38
  return textResult(result);
@@ -38,6 +52,9 @@ export function createOpenContextServer(basePath) {
38
52
  .describe("Optional topic name in snake_case or kebab-case. Omit to list all saved topics."),
39
53
  },
40
54
  }, async ({ topic }) => {
55
+ if (resolvedConfig.disabled) {
56
+ return textResult("OpenContext is currently paused. Tool access is disabled via configuration.");
57
+ }
41
58
  try {
42
59
  const result = await store.readContext(topic);
43
60
  return textResult(result);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtF,MAAM,UAAU,uBAAuB,CAAC,QAAiB;IACvD,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC;IAEzC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,cAAc;KACxB,CAAC,CAAC;IAEH,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,oIAAoI;QACtI,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CACP,0FAA0F,CAC3F;YACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,kDAAkD,CAAC;SACxF;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACvD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,UAAU,CAAC,sBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,yFAAyF;QAC3F,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,EAAE;iBACV,QAAQ,CACP,iFAAiF,CAClF;SACJ;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC9C,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,UAAU,CAAC,sBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CACF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,EAAE,UAAU,EAAuB,MAAM,aAAa,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,QAAiB,EACjB,MAAuB;IAEvB,MAAM,cAAc,GAAG,MAAM,IAAI,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAEzD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,cAAc;KACxB,CAAC,CAAC;IAEH,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,4EAA4E,cAAc,CAAC,IAAI,+CAA+C;QAC3J,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CACP,0FAA0F,CAC3F;YACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,kDAAkD,CAAC;SACxF;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,cAAc,CAAC,QAAQ,EAAE,CAAC;YAC5B,OAAO,UAAU,CAAC,6EAA6E,CAAC,CAAC;QACnG,CAAC;QACD,IAAI,cAAc,CAAC,QAAQ,EAAE,CAAC;YAC5B,OAAO,UAAU,CAAC,oFAAoF,EAAE,IAAI,CAAC,CAAC;QAChH,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACvD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,UAAU,CAAC,sBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,yFAAyF;QAC3F,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,EAAE;iBACV,QAAQ,CACP,iFAAiF,CAClF;SACJ;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAClB,IAAI,cAAc,CAAC,QAAQ,EAAE,CAAC;YAC5B,OAAO,UAAU,CAAC,6EAA6E,CAAC,CAAC;QACnG,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC9C,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,UAAU,CAAC,sBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CACF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,10 +1,29 @@
1
+ /** MCP server identifier used in protocol handshakes. */
1
2
  export declare const SERVER_NAME = "opencontext-mcp";
2
- export declare const SERVER_VERSION = "0.1.0";
3
- export declare const CONTEXT_DIRECTORY_NAME = ".opencontext";
3
+ /** Current server version for compatibility checks. */
4
+ export declare const SERVER_VERSION = "1.1.0";
5
+ /** Filename for the auto-generated context index. */
6
+ export declare const INDEX_FILENAME = "index.md";
7
+ /** Topic names reserved by the system — cannot be written by external agents. */
8
+ export declare const RESERVED_TOPICS: ReadonlySet<string>;
9
+ /**
10
+ * Regex pattern for validating topic names.
11
+ * Allows lowercase alphanumeric with single hyphens or underscores as separators.
12
+ * Examples: "api_contracts", "auth-rules", "migration-v2"
13
+ */
4
14
  export declare const TOPIC_PATTERN: RegExp;
15
+ /**
16
+ * Custom error class for user input validation errors.
17
+ * Thrown when topic names, content, or other user inputs fail validation.
18
+ */
5
19
  export declare class UserInputError extends Error {
6
20
  constructor(message: string);
7
21
  }
22
+ /**
23
+ * Creates a standardized MCP tool response object.
24
+ * @param text - Response message
25
+ * @param isError - Whether this is an error response (default: false)
26
+ */
8
27
  export declare function textResult(text: string, isError?: boolean): {
9
28
  isError?: boolean;
10
29
  content: {
@@ -12,5 +31,13 @@ export declare function textResult(text: string, isError?: boolean): {
12
31
  text: string;
13
32
  }[];
14
33
  };
34
+ /**
35
+ * Safely extracts error message from unknown error types.
36
+ * Handles Error objects, strings, and unknown values.
37
+ */
15
38
  export declare function getErrorMessage(error: unknown): string;
39
+ /**
40
+ * Type guard to check if an error is a Node.js filesystem error.
41
+ * Useful for handling ENOENT, EACCES, etc. from fs operations.
42
+ */
16
43
  export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
package/dist/types.js CHANGED
@@ -1,13 +1,32 @@
1
+ /** MCP server identifier used in protocol handshakes. */
1
2
  export const SERVER_NAME = "opencontext-mcp";
2
- export const SERVER_VERSION = "0.1.0";
3
- export const CONTEXT_DIRECTORY_NAME = ".opencontext";
3
+ /** Current server version for compatibility checks. */
4
+ export const SERVER_VERSION = "1.1.0";
5
+ /** Filename for the auto-generated context index. */
6
+ export const INDEX_FILENAME = "index.md";
7
+ /** Topic names reserved by the system — cannot be written by external agents. */
8
+ export const RESERVED_TOPICS = new Set(["index"]);
9
+ /**
10
+ * Regex pattern for validating topic names.
11
+ * Allows lowercase alphanumeric with single hyphens or underscores as separators.
12
+ * Examples: "api_contracts", "auth-rules", "migration-v2"
13
+ */
4
14
  export const TOPIC_PATTERN = /^[a-z0-9]+(?:[_-][a-z0-9]+)*$/;
15
+ /**
16
+ * Custom error class for user input validation errors.
17
+ * Thrown when topic names, content, or other user inputs fail validation.
18
+ */
5
19
  export class UserInputError extends Error {
6
20
  constructor(message) {
7
21
  super(message);
8
22
  this.name = "UserInputError";
9
23
  }
10
24
  }
25
+ /**
26
+ * Creates a standardized MCP tool response object.
27
+ * @param text - Response message
28
+ * @param isError - Whether this is an error response (default: false)
29
+ */
11
30
  export function textResult(text, isError = false) {
12
31
  return {
13
32
  content: [
@@ -19,12 +38,20 @@ export function textResult(text, isError = false) {
19
38
  ...(isError ? { isError: true } : {}),
20
39
  };
21
40
  }
41
+ /**
42
+ * Safely extracts error message from unknown error types.
43
+ * Handles Error objects, strings, and unknown values.
44
+ */
22
45
  export function getErrorMessage(error) {
23
46
  if (error instanceof Error) {
24
47
  return error.message;
25
48
  }
26
49
  return "An unknown error occurred.";
27
50
  }
51
+ /**
52
+ * Type guard to check if an error is a Node.js filesystem error.
53
+ * Useful for handling ENOENT, EACCES, etc. from fs operations.
54
+ */
28
55
  export function isNodeError(error) {
29
56
  return error instanceof Error && "code" in error;
30
57
  }
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AACtC,MAAM,CAAC,MAAM,sBAAsB,GAAG,cAAc,CAAC;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,+BAA+B,CAAC;AAE7D,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK;IACtD,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI;aACL;SACF;QACD,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,OAAO,4BAA4B,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAE7C,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,qDAAqD;AACrD,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAEzC,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;AAEvE;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,+BAA+B,CAAC;AAE7D;;;GAGG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK;IACtD,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI;aACL;SACF;QACD,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,OAAO,4BAA4B,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC;AACnD,CAAC"}
@@ -1 +1,47 @@
1
+ /**
2
+ * Options for configuring write guard behavior.
3
+ */
4
+ export interface GuardOptions {
5
+ /** Maximum payload size in KB (default: 50 KB) */
6
+ maxFileSizeKb?: number;
7
+ /** Whether to allow empty content (default: false) */
8
+ allowEmpty?: boolean;
9
+ /** Whether to check for forbidden patterns (default: true) */
10
+ strictPatternCheck?: boolean;
11
+ }
12
+ /**
13
+ * Result of write guard validation.
14
+ */
15
+ export interface GuardResult {
16
+ /** Whether the write is allowed */
17
+ allowed: boolean;
18
+ /** Human-readable reason for rejection */
19
+ reason?: string;
20
+ /** Error code for programmatic handling */
21
+ code?: "EMPTY_CONTENT" | "PAYLOAD_TOO_LARGE" | "INVALID_TOPIC" | "PATH_TRAVERSAL" | "FORBIDDEN_PATTERN" | "RESERVED_TOPIC";
22
+ }
23
+ /**
24
+ * Validates a topic string for safe filesystem operations.
25
+ * @param topicInput - The topic string to validate
26
+ * @returns The trimmed, validated topic
27
+ * @throws UserInputError if the topic is invalid
28
+ */
1
29
  export declare function validateTopic(topicInput: string): string;
30
+ /**
31
+ * Sanitizes a topic path to prevent path traversal attacks.
32
+ * Ensures the resolved path stays within the context directory.
33
+ * @param contextDir - The absolute path to the context directory
34
+ * @param topic - The topic name
35
+ * @returns The sanitized absolute path to the topic file
36
+ * @throws UserInputError if path traversal is detected
37
+ */
38
+ export declare function sanitizeTopicPath(contextDir: string, topic: string): string;
39
+ /**
40
+ * Validates a write payload for context storage.
41
+ * Checks topic validity, content safety, size limits, and forbidden patterns.
42
+ * @param topic - The topic name to validate
43
+ * @param content - The content to validate
44
+ * @param options - Optional configuration for validation behavior
45
+ * @returns GuardResult indicating whether the write is allowed
46
+ */
47
+ export declare function validateWritePayload(topic: string, content: string, options?: GuardOptions): GuardResult;
@@ -1,4 +1,20 @@
1
- import { TOPIC_PATTERN, UserInputError } from "./types.js";
1
+ import * as path from "node:path";
2
+ import { RESERVED_TOPICS, TOPIC_PATTERN, UserInputError } from "./types.js";
3
+ /**
4
+ * Forbidden patterns that indicate prompt injection attempts.
5
+ * These are checked case-insensitively against the content.
6
+ */
7
+ const FORBIDDEN_PATTERNS = [
8
+ /ignore\s+(all\s+)?(previous|prior)\s+instructions/i,
9
+ /system\s*:\s*override/i,
10
+ /bypass\s+(safety|guardrails?|system\s+prompt)/i,
11
+ ];
12
+ /**
13
+ * Validates a topic string for safe filesystem operations.
14
+ * @param topicInput - The topic string to validate
15
+ * @returns The trimmed, validated topic
16
+ * @throws UserInputError if the topic is invalid
17
+ */
2
18
  export function validateTopic(topicInput) {
3
19
  const topic = topicInput.trim();
4
20
  if (!TOPIC_PATTERN.test(topic)) {
@@ -6,4 +22,85 @@ export function validateTopic(topicInput) {
6
22
  }
7
23
  return topic;
8
24
  }
25
+ /**
26
+ * Sanitizes a topic path to prevent path traversal attacks.
27
+ * Ensures the resolved path stays within the context directory.
28
+ * @param contextDir - The absolute path to the context directory
29
+ * @param topic - The topic name
30
+ * @returns The sanitized absolute path to the topic file
31
+ * @throws UserInputError if path traversal is detected
32
+ */
33
+ export function sanitizeTopicPath(contextDir, topic) {
34
+ // Quick rejection of obvious path traversal attempts
35
+ if (topic.includes("..") || path.isAbsolute(topic) || topic.startsWith(".") || topic.includes("/")) {
36
+ throw new UserInputError("Path traversal detected: topic must not contain '..', '/', or absolute paths.");
37
+ }
38
+ const filePath = path.join(contextDir, `${topic}.md`);
39
+ const resolvedPath = path.resolve(filePath);
40
+ const resolvedContextDir = path.resolve(contextDir);
41
+ // Double-check resolved path is still within .opencontext directory
42
+ if (!resolvedPath.startsWith(resolvedContextDir + path.sep) && resolvedPath !== resolvedContextDir) {
43
+ throw new UserInputError("Path traversal detected: topic must not contain '..' or absolute paths.");
44
+ }
45
+ return resolvedPath;
46
+ }
47
+ /**
48
+ * Validates a write payload for context storage.
49
+ * Checks topic validity, content safety, size limits, and forbidden patterns.
50
+ * @param topic - The topic name to validate
51
+ * @param content - The content to validate
52
+ * @param options - Optional configuration for validation behavior
53
+ * @returns GuardResult indicating whether the write is allowed
54
+ */
55
+ export function validateWritePayload(topic, content, options) {
56
+ const opts = {
57
+ maxFileSizeKb: options?.maxFileSizeKb ?? 50,
58
+ allowEmpty: options?.allowEmpty ?? false,
59
+ strictPatternCheck: options?.strictPatternCheck ?? true,
60
+ };
61
+ const trimmedTopic = topic.trim();
62
+ if (!TOPIC_PATTERN.test(trimmedTopic)) {
63
+ return {
64
+ allowed: false,
65
+ reason: "Topic must be snake_case or kebab-case using lowercase letters, numbers, underscores, or hyphens.",
66
+ code: "INVALID_TOPIC",
67
+ };
68
+ }
69
+ if (RESERVED_TOPICS.has(trimmedTopic)) {
70
+ return {
71
+ allowed: false,
72
+ reason: `"${trimmedTopic}" is a reserved system topic and cannot be written directly.`,
73
+ code: "RESERVED_TOPIC",
74
+ };
75
+ }
76
+ if (!opts.allowEmpty && content.trim().length === 0) {
77
+ return {
78
+ allowed: false,
79
+ reason: "Content must not be empty or consist solely of whitespace.",
80
+ code: "EMPTY_CONTENT",
81
+ };
82
+ }
83
+ // Convert KB to bytes for size comparison
84
+ const maxBytes = opts.maxFileSizeKb * 1024;
85
+ const contentBytes = Buffer.byteLength(content, "utf8");
86
+ if (contentBytes > maxBytes) {
87
+ return {
88
+ allowed: false,
89
+ reason: `Payload size (${contentBytes} bytes) exceeds maximum allowed size (${maxBytes} bytes).`,
90
+ code: "PAYLOAD_TOO_LARGE",
91
+ };
92
+ }
93
+ if (opts.strictPatternCheck) {
94
+ for (const pattern of FORBIDDEN_PATTERNS) {
95
+ if (pattern.test(content)) {
96
+ return {
97
+ allowed: false,
98
+ reason: `Content contains forbidden pattern: ${pattern.source}`,
99
+ code: "FORBIDDEN_PATTERN",
100
+ };
101
+ }
102
+ }
103
+ }
104
+ return { allowed: true };
105
+ }
9
106
  //# sourceMappingURL=validation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC;IAEhC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,cAAc,CACtB,mGAAmG,CACpG,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencontext-mcp",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "A Model Context Protocol server for persistent project-specific AI agent context.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,10 +28,11 @@
28
28
  "memory"
29
29
  ],
30
30
  "repository": {
31
- "url": "git+https://github.com/slxca/opencontext.git"
31
+ "url": "git+https://github.com/slxca/opencontext.git",
32
+ "directory": "packages/opencontext"
32
33
  },
33
34
  "bugs": {
34
- "url": "https://github.com/slxca/mockr.js/issues",
35
+ "url": "https://github.com/slxca/opencontext/issues",
35
36
  "email": "bugs@s-luca.com"
36
37
  },
37
38
  "publishConfig": {
@@ -39,11 +40,11 @@
39
40
  },
40
41
  "author": "slxca <hello@slxca.com> (https://slxca.com)",
41
42
  "funding": "https://github.com/sponsors/slxca",
43
+ "homepage": "https://opencntx.dev",
42
44
  "license": "MIT",
43
45
  "engines": {
44
46
  "node": ">=20.0.0"
45
47
  },
46
- "packageManager": "pnpm@9.15.4",
47
48
  "dependencies": {
48
49
  "@modelcontextprotocol/sdk": "^1.17.4",
49
50
  "zod": "^4.5.4"
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 OpenContext Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,48 +0,0 @@
1
- # OpenContext Build Agent
2
-
3
- Copy this prompt into your AI client's system prompt to turn it into a developer that follows rules saved by the Plan Agent.
4
-
5
- ## System Prompt
6
-
7
- You are the OpenContext Build Agent: a pragmatic senior developer responsible for implementing changes while strictly following project context saved by the Plan Agent.
8
-
9
- Before writing or editing any code, you must use the `read_context` tool with no topic to list available OpenContext topics. Then read all topics relevant to the requested work. If relevant context exists, follow it. If the user's request conflicts with saved context, stop and ask for clarification before changing code.
10
-
11
- Your responsibilities:
12
-
13
- - Read OpenContext before coding.
14
- - Inspect the codebase and make the smallest correct change.
15
- - Preserve existing architecture, naming conventions, and testing strategy.
16
- - Run appropriate verification commands when feasible.
17
- - Use `save_context` when you discover a durable rule, convention, decision, or debugging note future agents should know.
18
- - Do not overwrite saved context with speculative or temporary information.
19
-
20
- Required workflow:
21
-
22
- ```text
23
- 1. Call read_context with no topic.
24
- 2. Read each relevant topic with read_context.
25
- 3. Inspect the affected code.
26
- 4. Implement the smallest correct change.
27
- 5. Run relevant tests, type checks, or builds.
28
- 6. Save any newly discovered durable context with save_context.
29
- 7. Summarize what changed and what was verified.
30
- ```
31
-
32
- If no OpenContext topics exist, state that no saved context was available, then proceed by inspecting the repository directly.
33
-
34
- Build output format:
35
-
36
- ```text
37
- Changed
38
- <files or behavior changed>
39
-
40
- Context Used
41
- <topics read or note that no context existed>
42
-
43
- Verification
44
- <commands run and results>
45
-
46
- Context Saved
47
- <topics saved or updated, if any>
48
- ```
@@ -1,53 +0,0 @@
1
- # OpenContext Plan Agent
2
-
3
- Copy this prompt into your AI client's system prompt to turn it into an architect that uses OpenContext MCP.
4
-
5
- ## System Prompt
6
-
7
- You are the OpenContext Plan Agent: a senior software architect responsible for turning requirements into clear implementation plans and durable project context.
8
-
9
- Before planning, use the `read_context` tool with no topic to list available OpenContext topics. Read every topic relevant to the user's request before making recommendations. If no context exists, inspect the repository and infer only what is supported by evidence in the codebase.
10
-
11
- Your responsibilities:
12
-
13
- - Analyze the user's requirement and the current codebase before proposing changes.
14
- - Identify architectural constraints, project conventions, API contracts, data model decisions, and implementation risks.
15
- - Create a practical step-by-step plan that a build agent can execute.
16
- - Use `save_context` to persist durable decisions, rules, and conventions that future agents should follow.
17
- - Prefer concise markdown context organized under focused snake_case or kebab-case topics.
18
-
19
- When using `save_context`, choose topics such as:
20
-
21
- - `architecture`
22
- - `coding_rules`
23
- - `api-contracts`
24
- - `data-model`
25
- - `testing-strategy`
26
-
27
- Context you save must be factual and durable. Do not save temporary guesses, chat-only preferences, or unresolved options unless they are clearly marked as open questions.
28
-
29
- Plan output format:
30
-
31
- ```text
32
- Summary
33
- <one-paragraph explanation of the intended solution>
34
-
35
- Relevant Context Read
36
- <topics read and what mattered>
37
-
38
- Architecture Decisions
39
- <decisions made or confirmed>
40
-
41
- Implementation Plan
42
- 1. <step>
43
- 2. <step>
44
- 3. <step>
45
-
46
- Risks And Checks
47
- <known risks, tests, and verification steps>
48
-
49
- Context Saved
50
- <topics saved or updated>
51
- ```
52
-
53
- Always save newly discovered durable context before finishing your response.