@contextstream/mcp-server 0.3.4 → 0.3.6

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 (2) hide show
  1. package/dist/index.js +529 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4449,6 +4449,106 @@ function resolveWorkspace(repoPath) {
4449
4449
  return { config: null, source: "ambiguous" };
4450
4450
  }
4451
4451
 
4452
+ // src/cache.ts
4453
+ var MemoryCache = class {
4454
+ constructor(cleanupIntervalMs = 6e4) {
4455
+ this.cache = /* @__PURE__ */ new Map();
4456
+ this.cleanupInterval = null;
4457
+ this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs);
4458
+ }
4459
+ /**
4460
+ * Get a cached value if it exists and hasn't expired
4461
+ */
4462
+ get(key) {
4463
+ const entry = this.cache.get(key);
4464
+ if (!entry) return void 0;
4465
+ if (Date.now() > entry.expiresAt) {
4466
+ this.cache.delete(key);
4467
+ return void 0;
4468
+ }
4469
+ return entry.value;
4470
+ }
4471
+ /**
4472
+ * Set a value with TTL in milliseconds
4473
+ */
4474
+ set(key, value, ttlMs) {
4475
+ this.cache.set(key, {
4476
+ value,
4477
+ expiresAt: Date.now() + ttlMs
4478
+ });
4479
+ }
4480
+ /**
4481
+ * Delete a specific key
4482
+ */
4483
+ delete(key) {
4484
+ return this.cache.delete(key);
4485
+ }
4486
+ /**
4487
+ * Delete all keys matching a prefix
4488
+ */
4489
+ deleteByPrefix(prefix) {
4490
+ let deleted = 0;
4491
+ for (const key of this.cache.keys()) {
4492
+ if (key.startsWith(prefix)) {
4493
+ this.cache.delete(key);
4494
+ deleted++;
4495
+ }
4496
+ }
4497
+ return deleted;
4498
+ }
4499
+ /**
4500
+ * Clear all cached entries
4501
+ */
4502
+ clear() {
4503
+ this.cache.clear();
4504
+ }
4505
+ /**
4506
+ * Remove expired entries
4507
+ */
4508
+ cleanup() {
4509
+ const now = Date.now();
4510
+ for (const [key, entry] of this.cache.entries()) {
4511
+ if (now > entry.expiresAt) {
4512
+ this.cache.delete(key);
4513
+ }
4514
+ }
4515
+ }
4516
+ /**
4517
+ * Stop the cleanup interval
4518
+ */
4519
+ destroy() {
4520
+ if (this.cleanupInterval) {
4521
+ clearInterval(this.cleanupInterval);
4522
+ this.cleanupInterval = null;
4523
+ }
4524
+ this.cache.clear();
4525
+ }
4526
+ };
4527
+ var CacheTTL = {
4528
+ // Workspace info rarely changes - cache for 5 minutes
4529
+ WORKSPACE: 5 * 60 * 1e3,
4530
+ // Project info rarely changes - cache for 5 minutes
4531
+ PROJECT: 5 * 60 * 1e3,
4532
+ // Session init context - cache for 60 seconds
4533
+ SESSION_INIT: 60 * 1e3,
4534
+ // Memory events - cache for 30 seconds (they change more often)
4535
+ MEMORY_EVENTS: 30 * 1e3,
4536
+ // Search results - cache for 60 seconds
4537
+ SEARCH: 60 * 1e3,
4538
+ // User preferences - cache for 5 minutes
4539
+ USER_PREFS: 5 * 60 * 1e3
4540
+ };
4541
+ var CacheKeys = {
4542
+ workspace: (id) => `workspace:${id}`,
4543
+ workspaceList: (userId) => `workspaces:${userId}`,
4544
+ project: (id) => `project:${id}`,
4545
+ projectList: (workspaceId) => `projects:${workspaceId}`,
4546
+ sessionInit: (workspaceId, projectId) => `session_init:${workspaceId || ""}:${projectId || ""}`,
4547
+ memoryEvents: (workspaceId) => `memory:${workspaceId}`,
4548
+ search: (query, workspaceId) => `search:${workspaceId || ""}:${query}`
4549
+ };
4550
+ var globalCache = new MemoryCache();
4551
+
4452
4552
  // src/client.ts
4453
4553
  var uuidSchema = external_exports.string().uuid();
4454
4554
  var ContextStreamClient = class {
@@ -4495,18 +4595,42 @@ var ContextStreamClient = class {
4495
4595
  uuidSchema.parse(projectId);
4496
4596
  return request(this.config, `/projects/${projectId}/index`, { body: {} });
4497
4597
  }
4498
- // Search
4598
+ // Search - each method adds required search_type and filters fields
4499
4599
  searchSemantic(body) {
4500
- return request(this.config, "/search/semantic", { body: this.withDefaults(body) });
4600
+ return request(this.config, "/search/semantic", {
4601
+ body: {
4602
+ ...this.withDefaults(body),
4603
+ search_type: "semantic",
4604
+ filters: body.workspace_id ? {} : { file_types: [], languages: [], file_paths: [], exclude_paths: [], content_types: [], tags: [] }
4605
+ }
4606
+ });
4501
4607
  }
4502
4608
  searchHybrid(body) {
4503
- return request(this.config, "/search/hybrid", { body: this.withDefaults(body) });
4609
+ return request(this.config, "/search/hybrid", {
4610
+ body: {
4611
+ ...this.withDefaults(body),
4612
+ search_type: "hybrid",
4613
+ filters: body.workspace_id ? {} : { file_types: [], languages: [], file_paths: [], exclude_paths: [], content_types: [], tags: [] }
4614
+ }
4615
+ });
4504
4616
  }
4505
4617
  searchKeyword(body) {
4506
- return request(this.config, "/search/keyword", { body: this.withDefaults(body) });
4618
+ return request(this.config, "/search/keyword", {
4619
+ body: {
4620
+ ...this.withDefaults(body),
4621
+ search_type: "keyword",
4622
+ filters: body.workspace_id ? {} : { file_types: [], languages: [], file_paths: [], exclude_paths: [], content_types: [], tags: [] }
4623
+ }
4624
+ });
4507
4625
  }
4508
4626
  searchPattern(body) {
4509
- return request(this.config, "/search/pattern", { body: this.withDefaults(body) });
4627
+ return request(this.config, "/search/pattern", {
4628
+ body: {
4629
+ ...this.withDefaults(body),
4630
+ search_type: "pattern",
4631
+ filters: body.workspace_id ? {} : { file_types: [], languages: [], file_paths: [], exclude_paths: [], content_types: [], tags: [] }
4632
+ }
4633
+ });
4510
4634
  }
4511
4635
  // Memory / Knowledge
4512
4636
  createMemoryEvent(body) {
@@ -4587,14 +4711,24 @@ var ContextStreamClient = class {
4587
4711
  aiEnhancedContext(body) {
4588
4712
  return request(this.config, "/ai/context/enhanced", { body: this.withDefaults(body) });
4589
4713
  }
4590
- // Project extended operations
4591
- getProject(projectId) {
4714
+ // Project extended operations (with caching)
4715
+ async getProject(projectId) {
4592
4716
  uuidSchema.parse(projectId);
4593
- return request(this.config, `/projects/${projectId}`, { method: "GET" });
4717
+ const cacheKey = CacheKeys.project(projectId);
4718
+ const cached = globalCache.get(cacheKey);
4719
+ if (cached) return cached;
4720
+ const result = await request(this.config, `/projects/${projectId}`, { method: "GET" });
4721
+ globalCache.set(cacheKey, result, CacheTTL.PROJECT);
4722
+ return result;
4594
4723
  }
4595
- projectOverview(projectId) {
4724
+ async projectOverview(projectId) {
4596
4725
  uuidSchema.parse(projectId);
4597
- return request(this.config, `/projects/${projectId}/overview`, { method: "GET" });
4726
+ const cacheKey = `project_overview:${projectId}`;
4727
+ const cached = globalCache.get(cacheKey);
4728
+ if (cached) return cached;
4729
+ const result = await request(this.config, `/projects/${projectId}/overview`, { method: "GET" });
4730
+ globalCache.set(cacheKey, result, CacheTTL.PROJECT);
4731
+ return result;
4598
4732
  }
4599
4733
  projectStatistics(projectId) {
4600
4734
  uuidSchema.parse(projectId);
@@ -4618,14 +4752,24 @@ var ContextStreamClient = class {
4618
4752
  body: { files }
4619
4753
  });
4620
4754
  }
4621
- // Workspace extended operations
4622
- getWorkspace(workspaceId) {
4755
+ // Workspace extended operations (with caching)
4756
+ async getWorkspace(workspaceId) {
4623
4757
  uuidSchema.parse(workspaceId);
4624
- return request(this.config, `/workspaces/${workspaceId}`, { method: "GET" });
4758
+ const cacheKey = CacheKeys.workspace(workspaceId);
4759
+ const cached = globalCache.get(cacheKey);
4760
+ if (cached) return cached;
4761
+ const result = await request(this.config, `/workspaces/${workspaceId}`, { method: "GET" });
4762
+ globalCache.set(cacheKey, result, CacheTTL.WORKSPACE);
4763
+ return result;
4625
4764
  }
4626
- workspaceOverview(workspaceId) {
4765
+ async workspaceOverview(workspaceId) {
4627
4766
  uuidSchema.parse(workspaceId);
4628
- return request(this.config, `/workspaces/${workspaceId}/overview`, { method: "GET" });
4767
+ const cacheKey = `workspace_overview:${workspaceId}`;
4768
+ const cached = globalCache.get(cacheKey);
4769
+ if (cached) return cached;
4770
+ const result = await request(this.config, `/workspaces/${workspaceId}/overview`, { method: "GET" });
4771
+ globalCache.set(cacheKey, result, CacheTTL.WORKSPACE);
4772
+ return result;
4629
4773
  }
4630
4774
  workspaceAnalytics(workspaceId) {
4631
4775
  uuidSchema.parse(workspaceId);
@@ -4833,49 +4977,102 @@ var ContextStreamClient = class {
4833
4977
  context.ide_roots = ideRoots;
4834
4978
  if (workspaceId) {
4835
4979
  try {
4836
- context.workspace = await this.workspaceOverview(workspaceId);
4837
- } catch {
4980
+ const batchedContext = await this._fetchSessionContextBatched({
4981
+ workspace_id: workspaceId,
4982
+ project_id: projectId,
4983
+ session_id: context.session_id,
4984
+ context_hint: params.context_hint,
4985
+ include_recent_memory: params.include_recent_memory !== false,
4986
+ include_decisions: params.include_decisions !== false
4987
+ });
4988
+ if (batchedContext.workspace) {
4989
+ context.workspace = batchedContext.workspace;
4990
+ }
4991
+ if (batchedContext.project) {
4992
+ context.project = batchedContext.project;
4993
+ }
4994
+ if (batchedContext.recent_memory) {
4995
+ context.recent_memory = { items: batchedContext.recent_memory };
4996
+ }
4997
+ if (batchedContext.recent_decisions) {
4998
+ context.recent_decisions = { items: batchedContext.recent_decisions };
4999
+ }
5000
+ if (batchedContext.relevant_context) {
5001
+ context.relevant_context = batchedContext.relevant_context;
5002
+ }
5003
+ } catch (e) {
5004
+ console.error("[ContextStream] Batched endpoint failed, falling back to individual calls:", e);
5005
+ await this._fetchSessionContextFallback(context, workspaceId, projectId, params);
4838
5006
  }
4839
5007
  }
5008
+ return context;
5009
+ }
5010
+ /**
5011
+ * Fetch session context using the batched /session/init endpoint.
5012
+ * This is much faster than making 5-6 individual API calls.
5013
+ */
5014
+ async _fetchSessionContextBatched(params) {
5015
+ const cacheKey = CacheKeys.sessionInit(params.workspace_id, params.project_id);
5016
+ const cached = globalCache.get(cacheKey);
5017
+ if (cached) {
5018
+ console.error("[ContextStream] Session context cache HIT");
5019
+ return cached;
5020
+ }
5021
+ const result = await request(this.config, "/session/init", {
5022
+ body: {
5023
+ workspace_id: params.workspace_id,
5024
+ project_id: params.project_id,
5025
+ session_id: params.session_id,
5026
+ include_recent_memory: params.include_recent_memory ?? true,
5027
+ include_decisions: params.include_decisions ?? true
5028
+ }
5029
+ });
5030
+ const contextData = "data" in result && result.data ? result.data : result;
5031
+ globalCache.set(cacheKey, contextData, CacheTTL.SESSION_INIT);
5032
+ return contextData;
5033
+ }
5034
+ /**
5035
+ * Fallback to individual API calls if batched endpoint is unavailable.
5036
+ */
5037
+ async _fetchSessionContextFallback(context, workspaceId, projectId, params) {
5038
+ try {
5039
+ context.workspace = await this.workspaceOverview(workspaceId);
5040
+ } catch {
5041
+ }
4840
5042
  if (projectId) {
4841
5043
  try {
4842
5044
  context.project = await this.projectOverview(projectId);
4843
5045
  } catch {
4844
5046
  }
4845
5047
  }
4846
- if (params.include_recent_memory !== false && workspaceId) {
5048
+ if (params.include_recent_memory !== false) {
4847
5049
  try {
4848
5050
  context.recent_memory = await this.listMemoryEvents({
4849
5051
  workspace_id: workspaceId,
4850
- // NOTE: Intentionally NOT filtering by project_id
4851
- // to get workspace-wide context
4852
5052
  limit: 10
4853
5053
  });
4854
5054
  } catch {
4855
5055
  }
4856
5056
  }
4857
- if (params.include_decisions !== false && workspaceId) {
5057
+ if (params.include_decisions !== false) {
4858
5058
  try {
4859
5059
  context.recent_decisions = await this.memoryDecisions({
4860
5060
  workspace_id: workspaceId,
4861
- // NOTE: Intentionally NOT filtering by project_id
4862
5061
  limit: 5
4863
5062
  });
4864
5063
  } catch {
4865
5064
  }
4866
5065
  }
4867
- if (params.context_hint && workspaceId) {
5066
+ if (params.context_hint) {
4868
5067
  try {
4869
5068
  context.relevant_context = await this.memorySearch({
4870
5069
  query: params.context_hint,
4871
5070
  workspace_id: workspaceId,
4872
- // Search workspace-wide first
4873
5071
  limit: 5
4874
5072
  });
4875
5073
  } catch {
4876
5074
  }
4877
5075
  }
4878
- return context;
4879
5076
  }
4880
5077
  /**
4881
5078
  * Associate a folder with a workspace (called after user selects from candidates).
@@ -5015,6 +5212,188 @@ var ContextStreamClient = class {
5015
5212
  }
5016
5213
  };
5017
5214
 
5215
+ // src/rules-templates.ts
5216
+ var CONTEXTSTREAM_RULES = `
5217
+ ## ContextStream Integration
5218
+
5219
+ You have access to ContextStream MCP tools for persistent memory and context across sessions.
5220
+
5221
+ ---
5222
+
5223
+ ### \u{1F6A8} ESSENTIAL Tools (Always Use)
5224
+
5225
+ These tools should be called automatically in every conversation:
5226
+
5227
+ | When | Tool | Example Prompt |
5228
+ |------|------|----------------|
5229
+ | **Conversation start** | \`session_init\` | "Load my workspace context" |
5230
+ | **User makes a decision** | \`session_capture\` | "Remember we decided to use PostgreSQL" |
5231
+ | **User states a preference** | \`session_capture\` | "Remember I prefer TypeScript strict mode" |
5232
+ | **Before starting work** | \`session_recall\` | "What did we decide about auth?" |
5233
+
5234
+ ---
5235
+
5236
+ ### \u{1F4DD} Memory & Context Tools (Use Frequently)
5237
+
5238
+ | Tool | Example Prompt |
5239
+ |------|----------------|
5240
+ | \`session_remember\` | "Remember that the API uses JWT tokens" |
5241
+ | \`session_smart_search\` | "Search for everything about payment processing" |
5242
+ | \`memory_search\` | "Search my notes for caching strategy" |
5243
+ | \`memory_decisions\` | "Show me all past decisions" |
5244
+ | \`memory_timeline\` | "Show me the timeline of this workspace" |
5245
+ | \`memory_summary\` | "Summarize what we've worked on" |
5246
+
5247
+ ---
5248
+
5249
+ ### \u{1F50D} Code Search Tools (Use When Exploring Code)
5250
+
5251
+ | Tool | Example Prompt |
5252
+ |------|----------------|
5253
+ | \`search_semantic\` | "How does authentication work in this codebase?" |
5254
+ | \`search_hybrid\` | "Find code related to user permissions" |
5255
+ | \`search_keyword\` | "Search for handleSubmit function" |
5256
+ | \`search_pattern\` | "Find all TODO comments" |
5257
+ | \`ai_context\` | "Build context for implementing user roles" |
5258
+ | \`ai_enhanced_context\` | "Give me deep context on the payment system" |
5259
+
5260
+ ---
5261
+
5262
+ ### \u{1F578}\uFE0F Code Analysis Tools (Use for Architecture)
5263
+
5264
+ | Tool | Example Prompt |
5265
+ |------|----------------|
5266
+ | \`graph_dependencies\` | "What does UserService depend on?" |
5267
+ | \`graph_impact\` | "What would be affected if I change the User model?" |
5268
+ | \`graph_call_path\` | "How does login() eventually call the database?" |
5269
+ | \`graph_circular_dependencies\` | "Are there any circular dependencies?" |
5270
+ | \`graph_unused_code\` | "Find unused code in this project" |
5271
+
5272
+ ---
5273
+
5274
+ ### \u{1F4C1} Project & Workspace Tools
5275
+
5276
+ | Tool | Example Prompt |
5277
+ |------|----------------|
5278
+ | \`projects_overview\` | "Give me an overview of this project" |
5279
+ | \`projects_index\` | "Re-index this project" |
5280
+ | \`workspaces_overview\` | "Give me a workspace overview" |
5281
+ | \`workspace_associate\` | "Associate this folder with my workspace" |
5282
+ | \`generate_editor_rules\` | "Generate ContextStream rules for this project" |
5283
+
5284
+ ---
5285
+
5286
+ ### Automatic Behavior Guidelines
5287
+
5288
+ **At conversation start:**
5289
+ - Call \`session_init\` to load context (this happens automatically on first tool call)
5290
+ - Review the loaded decisions and memory before responding
5291
+
5292
+ **During the conversation:**
5293
+ - When the user makes a decision \u2192 \`session_capture\` with \`event_type: "decision"\`
5294
+ - When you learn a user preference \u2192 \`session_capture\` with \`event_type: "preference"\`
5295
+ - When discovering important context \u2192 \`session_remember\`
5296
+ - When encountering a bug \u2192 \`session_capture\` with \`event_type: "bug"\`
5297
+ - When a feature is discussed \u2192 \`session_capture\` with \`event_type: "feature"\`
5298
+
5299
+ **When exploring code:**
5300
+ - Use \`search_semantic\` or \`search_hybrid\` for code search
5301
+ - Use \`graph_dependencies\` to understand architecture
5302
+ - Use \`graph_impact\` before making changes
5303
+
5304
+ **At conversation end (if significant work done):**
5305
+ - Summarize and capture key decisions with \`session_capture\`
5306
+
5307
+ ---
5308
+
5309
+ ### Quick Examples
5310
+
5311
+ \`\`\`
5312
+ # Start of conversation - load context
5313
+ session_init()
5314
+
5315
+ # User says "Let's use PostgreSQL for the database"
5316
+ session_capture(event_type="decision", title="Database Choice", content="Decided to use PostgreSQL...")
5317
+
5318
+ # User says "I prefer async/await over callbacks"
5319
+ session_capture(event_type="preference", title="Async Style", content="User prefers async/await...")
5320
+
5321
+ # Need to find related code
5322
+ search_semantic(query="authentication middleware")
5323
+
5324
+ # Check what we discussed before
5325
+ session_recall(query="what did we decide about caching?")
5326
+ \`\`\`
5327
+ `.trim();
5328
+ var TEMPLATES = {
5329
+ windsurf: {
5330
+ filename: ".windsurfrules",
5331
+ description: "Windsurf AI rules",
5332
+ content: `# Windsurf Rules
5333
+ ${CONTEXTSTREAM_RULES}
5334
+ `
5335
+ },
5336
+ cursor: {
5337
+ filename: ".cursorrules",
5338
+ description: "Cursor AI rules",
5339
+ content: `# Cursor Rules
5340
+ ${CONTEXTSTREAM_RULES}
5341
+ `
5342
+ },
5343
+ cline: {
5344
+ filename: ".clinerules",
5345
+ description: "Cline AI rules",
5346
+ content: `# Cline Rules
5347
+ ${CONTEXTSTREAM_RULES}
5348
+ `
5349
+ },
5350
+ claude: {
5351
+ filename: "CLAUDE.md",
5352
+ description: "Claude Code instructions",
5353
+ content: `# Claude Code Instructions
5354
+ ${CONTEXTSTREAM_RULES}
5355
+ `
5356
+ },
5357
+ aider: {
5358
+ filename: ".aider.conf.yml",
5359
+ description: "Aider configuration with system prompt",
5360
+ content: `# Aider Configuration
5361
+ # Note: Aider uses different config format - this adds to the system prompt
5362
+
5363
+ # Add ContextStream guidance to conventions
5364
+ conventions: |
5365
+ ${CONTEXTSTREAM_RULES.split("\n").map((line) => " " + line).join("\n")}
5366
+ `
5367
+ }
5368
+ };
5369
+ function getAvailableEditors() {
5370
+ return Object.keys(TEMPLATES);
5371
+ }
5372
+ function getTemplate(editor) {
5373
+ return TEMPLATES[editor.toLowerCase()] || null;
5374
+ }
5375
+ function generateRuleContent(editor, options) {
5376
+ const template = getTemplate(editor);
5377
+ if (!template) return null;
5378
+ let content = template.content;
5379
+ if (options?.workspaceName || options?.projectName) {
5380
+ const header = `
5381
+ # Workspace: ${options.workspaceName || "Unknown"}
5382
+ ${options.projectName ? `# Project: ${options.projectName}` : ""}
5383
+ ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
5384
+
5385
+ `;
5386
+ content = header + content;
5387
+ }
5388
+ if (options?.additionalRules) {
5389
+ content += "\n\n## Project-Specific Rules\n\n" + options.additionalRules;
5390
+ }
5391
+ return {
5392
+ filename: template.filename,
5393
+ content: content.trim() + "\n"
5394
+ };
5395
+ }
5396
+
5018
5397
  // src/tools.ts
5019
5398
  function formatContent(data) {
5020
5399
  return JSON.stringify(data, null, 2);
@@ -5053,10 +5432,23 @@ function registerTools(server, client, sessionManager) {
5053
5432
  };
5054
5433
  }
5055
5434
  function registerTool(name, config, handler) {
5435
+ const safeHandler = async (input) => {
5436
+ try {
5437
+ return await handler(input);
5438
+ } catch (error) {
5439
+ const errorMessage = error?.message || String(error);
5440
+ const errorDetails = error?.body || error?.details || null;
5441
+ const errorCode = error?.code || error?.status || "UNKNOWN_ERROR";
5442
+ const serializedError = new Error(
5443
+ `[${errorCode}] ${errorMessage}${errorDetails ? `: ${JSON.stringify(errorDetails)}` : ""}`
5444
+ );
5445
+ throw serializedError;
5446
+ }
5447
+ };
5056
5448
  server.registerTool(
5057
5449
  name,
5058
5450
  config,
5059
- wrapWithAutoContext(name, handler)
5451
+ wrapWithAutoContext(name, safeHandler)
5060
5452
  );
5061
5453
  }
5062
5454
  registerTool(
@@ -5882,17 +6274,52 @@ Use this to understand how the user likes to work and adapt your responses accor
5882
6274
  description: `Associate a folder/repo with a workspace after user selection.
5883
6275
  Call this after session_init returns status='requires_workspace_selection' and the user has chosen a workspace.
5884
6276
  This persists the selection to .contextstream/config.json so future sessions auto-connect.
5885
- Optionally creates a parent folder mapping (e.g., all repos under /dev/company/* map to the same workspace).`,
6277
+ Optionally creates a parent folder mapping (e.g., all repos under /dev/company/* map to the same workspace).
6278
+ Optionally generates AI editor rules for automatic ContextStream usage.`,
5886
6279
  inputSchema: external_exports.object({
5887
6280
  folder_path: external_exports.string().describe("Absolute path to the folder/repo to associate"),
5888
6281
  workspace_id: external_exports.string().uuid().describe("Workspace ID to associate with"),
5889
6282
  workspace_name: external_exports.string().optional().describe("Workspace name for reference"),
5890
- create_parent_mapping: external_exports.boolean().optional().describe("Also create a parent folder mapping (e.g., /dev/maker/* -> workspace)")
6283
+ create_parent_mapping: external_exports.boolean().optional().describe("Also create a parent folder mapping (e.g., /dev/maker/* -> workspace)"),
6284
+ generate_editor_rules: external_exports.boolean().optional().describe("Generate AI editor rules (.windsurfrules, .cursorrules, etc.) for automatic ContextStream usage")
5891
6285
  })
5892
6286
  },
5893
6287
  async (input) => {
5894
6288
  const result = await client.associateWorkspace(input);
5895
- return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
6289
+ let rulesGenerated = [];
6290
+ if (input.generate_editor_rules) {
6291
+ const fs3 = await import("fs");
6292
+ const path3 = await import("path");
6293
+ for (const editor of getAvailableEditors()) {
6294
+ const rule = generateRuleContent(editor, {
6295
+ workspaceName: input.workspace_name,
6296
+ workspaceId: input.workspace_id
6297
+ });
6298
+ if (rule) {
6299
+ const filePath = path3.join(input.folder_path, rule.filename);
6300
+ try {
6301
+ let existingContent = "";
6302
+ try {
6303
+ existingContent = fs3.readFileSync(filePath, "utf-8");
6304
+ } catch {
6305
+ }
6306
+ if (!existingContent) {
6307
+ fs3.writeFileSync(filePath, rule.content);
6308
+ rulesGenerated.push(rule.filename);
6309
+ } else if (!existingContent.includes("ContextStream Integration")) {
6310
+ fs3.writeFileSync(filePath, existingContent + "\n\n" + rule.content);
6311
+ rulesGenerated.push(rule.filename + " (appended)");
6312
+ }
6313
+ } catch {
6314
+ }
6315
+ }
6316
+ }
6317
+ }
6318
+ const response = {
6319
+ ...result,
6320
+ editor_rules_generated: rulesGenerated.length > 0 ? rulesGenerated : void 0
6321
+ };
6322
+ return { content: [{ type: "text", text: formatContent(response) }], structuredContent: toStructured(response) };
5896
6323
  }
5897
6324
  );
5898
6325
  registerTool(
@@ -5992,6 +6419,79 @@ Example: "What were the auth decisions?" or "What are my TypeScript preferences?
5992
6419
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
5993
6420
  }
5994
6421
  );
6422
+ registerTool(
6423
+ "generate_editor_rules",
6424
+ {
6425
+ title: "Generate editor AI rules",
6426
+ description: `Generate AI rule files for editors (Windsurf, Cursor, Cline, Claude Code, Aider).
6427
+ These rules instruct the AI to automatically use ContextStream for memory and context.
6428
+ Supported editors: ${getAvailableEditors().join(", ")}`,
6429
+ inputSchema: external_exports.object({
6430
+ folder_path: external_exports.string().describe("Absolute path to the project folder"),
6431
+ editors: external_exports.array(external_exports.enum(["windsurf", "cursor", "cline", "claude", "aider", "all"])).optional().describe("Which editors to generate rules for. Defaults to all."),
6432
+ workspace_name: external_exports.string().optional().describe("Workspace name to include in rules"),
6433
+ workspace_id: external_exports.string().uuid().optional().describe("Workspace ID to include in rules"),
6434
+ project_name: external_exports.string().optional().describe("Project name to include in rules"),
6435
+ additional_rules: external_exports.string().optional().describe("Additional project-specific rules to append"),
6436
+ dry_run: external_exports.boolean().optional().describe("If true, return content without writing files")
6437
+ })
6438
+ },
6439
+ async (input) => {
6440
+ const fs3 = await import("fs");
6441
+ const path3 = await import("path");
6442
+ const editors = input.editors?.includes("all") || !input.editors ? getAvailableEditors() : input.editors.filter((e) => e !== "all");
6443
+ const results = [];
6444
+ for (const editor of editors) {
6445
+ const rule = generateRuleContent(editor, {
6446
+ workspaceName: input.workspace_name,
6447
+ workspaceId: input.workspace_id,
6448
+ projectName: input.project_name,
6449
+ additionalRules: input.additional_rules
6450
+ });
6451
+ if (!rule) {
6452
+ results.push({ editor, filename: "", status: "unknown editor" });
6453
+ continue;
6454
+ }
6455
+ const filePath = path3.join(input.folder_path, rule.filename);
6456
+ if (input.dry_run) {
6457
+ results.push({
6458
+ editor,
6459
+ filename: rule.filename,
6460
+ status: "dry run - would create",
6461
+ content: rule.content
6462
+ });
6463
+ } else {
6464
+ try {
6465
+ let existingContent = "";
6466
+ try {
6467
+ existingContent = fs3.readFileSync(filePath, "utf-8");
6468
+ } catch {
6469
+ }
6470
+ if (existingContent && !existingContent.includes("ContextStream Integration")) {
6471
+ const updatedContent = existingContent + "\n\n" + rule.content;
6472
+ fs3.writeFileSync(filePath, updatedContent);
6473
+ results.push({ editor, filename: rule.filename, status: "appended to existing" });
6474
+ } else {
6475
+ fs3.writeFileSync(filePath, rule.content);
6476
+ results.push({ editor, filename: rule.filename, status: "created" });
6477
+ }
6478
+ } catch (err) {
6479
+ results.push({
6480
+ editor,
6481
+ filename: rule.filename,
6482
+ status: `error: ${err.message}`
6483
+ });
6484
+ }
6485
+ }
6486
+ }
6487
+ const summary = {
6488
+ folder: input.folder_path,
6489
+ results,
6490
+ message: input.dry_run ? "Dry run complete. Use dry_run: false to write files." : `Generated ${results.filter((r) => r.status === "created" || r.status.includes("appended")).length} rule files.`
6491
+ };
6492
+ return { content: [{ type: "text", text: formatContent(summary) }], structuredContent: toStructured(summary) };
6493
+ }
6494
+ );
5995
6495
  }
5996
6496
 
5997
6497
  // src/resources.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextstream/mcp-server",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "MCP server exposing ContextStream public API - code context, memory, search, and AI tools for developers",
5
5
  "type": "module",
6
6
  "license": "MIT",