@splicr/mcp-server 0.10.3 → 0.11.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/dist/cli.js CHANGED
@@ -713,31 +713,23 @@ async function runSessionEnd() {
713
713
  // Take the last ~5000 chars of assistant messages
714
714
  const combined = messages.join('\n\n');
715
715
  const tail = combined.length > 5000 ? combined.slice(-5000) : combined;
716
- // Skip if agent already saved to Splicr during this session
717
- if (tail.includes('Saved to Splicr')) {
718
- process.exit(0);
719
- return;
720
- }
721
- // Send to capture endpoint — let the AI pipeline distill and decide significance
716
+ // Send to structured learning extraction endpoint
722
717
  const authMod = await import('./auth.js');
723
718
  const auth = await authMod.loadAuth();
724
719
  const API_URL = process.env.SPLICR_API_URL || 'https://api-production-d889.up.railway.app';
725
- // Fire-and-forget POST to capture endpoint
726
- fetch(`${API_URL}/capture`, {
720
+ // Fire-and-forget POST to session/complete-with-transcript
721
+ // This replaces the old raw transcript dump with structured AI extraction
722
+ fetch(`${API_URL}/mcp/session/complete-with-transcript`, {
727
723
  method: 'POST',
728
724
  headers: {
729
725
  'Authorization': `Bearer ${auth.accessToken}`,
730
726
  'Content-Type': 'application/json',
731
727
  },
732
728
  body: JSON.stringify({
733
- content: tail,
734
- source_type: 'agent',
735
- captured_from: 'ide_agent',
736
- title: `Session learning — ${new Date().toISOString().split('T')[0]}`,
737
- tags: ['auto-save', 'session-learning'],
738
- extracted_content: tail,
729
+ session_id: sessionId || undefined,
730
+ transcript_tail: tail,
739
731
  }),
740
- signal: AbortSignal.timeout(8000),
732
+ signal: AbortSignal.timeout(10000),
741
733
  }).catch(() => { }); // fire-and-forget
742
734
  process.exit(0);
743
735
  }
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ import { browseProjectSchema, handleBrowseProject } from './tools/browse-project
13
13
  import { getCompiledPageSchema, handleGetCompiledPage } from './tools/get-compiled-page.js';
14
14
  import { grepKnowledgeSchema, handleGrepKnowledge } from './tools/grep-knowledge.js';
15
15
  import { exploreKnowledgeSchema, handleExploreKnowledge } from './tools/explore-knowledge.js';
16
+ import { getDecisionsSchema, handleGetDecisions } from './tools/get-decisions.js';
17
+ import { getTeamStatusSchema, handleGetTeamStatus } from './tools/get-team-status.js';
16
18
  import { completeSession } from './lib/api-client.js';
17
19
  // Prevent unhandled errors from crashing the MCP server
18
20
  process.on('uncaughtException', (err) => {
@@ -61,6 +63,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
61
63
  getCompiledPageSchema,
62
64
  grepKnowledgeSchema,
63
65
  exploreKnowledgeSchema,
66
+ getDecisionsSchema,
67
+ getTeamStatusSchema,
64
68
  ],
65
69
  }));
66
70
  // Handle tool calls with per-tool timeout
@@ -79,6 +83,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
79
83
  get_compiled_page: handleGetCompiledPage,
80
84
  grep_knowledge: handleGrepKnowledge,
81
85
  explore_knowledge: handleExploreKnowledge,
86
+ get_decisions: handleGetDecisions,
87
+ get_team_status: handleGetTeamStatus,
82
88
  }[name];
83
89
  if (!handler) {
84
90
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
@@ -11,6 +11,7 @@ export declare function getProjectContext(params: {
11
11
  limit?: number;
12
12
  }): Promise<{
13
13
  results: any[];
14
+ patterns?: any[];
14
15
  project_name?: string;
15
16
  }>;
16
17
  export declare function getRecentInsights(params: {
@@ -27,6 +28,7 @@ export declare function saveFromAgent(params: {
27
28
  tags?: string[];
28
29
  memory_type?: string;
29
30
  context?: Record<string, unknown>;
31
+ register_as_pattern?: boolean;
30
32
  }): Promise<{
31
33
  id: string;
32
34
  title: string;
@@ -136,4 +138,15 @@ export declare function getRelevantContext(params: {
136
138
  results: any[];
137
139
  search_strategy: string;
138
140
  }>;
141
+ export declare function getDecisions(params: {
142
+ topic?: string;
143
+ project_name?: string;
144
+ type?: string;
145
+ limit?: number;
146
+ }): Promise<{
147
+ results: any[];
148
+ }>;
149
+ export declare function getTeamStatus(params: {
150
+ project?: string;
151
+ }): Promise<any>;
139
152
  export { API_URL };
@@ -100,4 +100,12 @@ export async function getRelevantContext(params) {
100
100
  const data = await apiRequest('POST', '/mcp/relevant-context', params);
101
101
  return { results: data.results ?? [], search_strategy: data.search_strategy ?? '' };
102
102
  }
103
+ export async function getDecisions(params) {
104
+ const data = await apiRequest('POST', '/mcp/decisions', params);
105
+ return { results: data.results ?? [] };
106
+ }
107
+ export async function getTeamStatus(params) {
108
+ const query = params.project ? `?project=${encodeURIComponent(params.project)}` : '';
109
+ return await apiRequest('GET', `/mcp/team-status${query}`);
110
+ }
103
111
  export { API_URL };
@@ -68,7 +68,8 @@ export async function handleBrowseProject(args) {
68
68
  const cat = c.category || '';
69
69
  const preview = c.insight_preview || '';
70
70
  const projects = c.projects?.length > 0 ? c.projects.join(', ') : '';
71
- let line = `${idx}. **${c.title || 'Untitled'}**${relevance}\n`;
71
+ const teammate = c.from_teammate ? ' (from teammate)' : '';
72
+ let line = `${idx}. **${c.title || 'Untitled'}**${teammate}${relevance}\n`;
72
73
  line += ` ${cat}${source ? ' · ' + source : ''} · ${date}`;
73
74
  if (projects)
74
75
  line += ` · → ${projects}`;
@@ -0,0 +1,27 @@
1
+ export declare const getDecisionsSchema: {
2
+ name: "get_decisions";
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ topic: {
8
+ type: "string";
9
+ description: string;
10
+ };
11
+ project: {
12
+ type: "string";
13
+ description: string;
14
+ };
15
+ type: {
16
+ type: "string";
17
+ enum: string[];
18
+ description: string;
19
+ };
20
+ limit: {
21
+ type: "number";
22
+ description: string;
23
+ };
24
+ };
25
+ };
26
+ };
27
+ export declare function handleGetDecisions(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,90 @@
1
+ import { getDecisions } from '../lib/api-client.js';
2
+ import { detectProject } from '../lib/project-detector.js';
3
+ import * as session from '../lib/session-state.js';
4
+ export const getDecisionsSchema = {
5
+ name: 'get_decisions',
6
+ description: `Query the team's decision log - technical decisions, patterns, discoveries, and failures recorded during agent sessions.
7
+
8
+ Use when:
9
+ - Before making an architecture or technology choice (see what the team decided before)
10
+ - When someone asks "why did we do X this way?"
11
+ - When evaluating alternatives for a new approach
12
+ - To check if something was tried before and failed
13
+
14
+ Returns structured decisions with rationale, alternatives considered, and anti-pattern warnings.`,
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ topic: { type: 'string', description: 'Topic to search decisions about (e.g., "auth", "caching", "error handling")' },
19
+ project: { type: 'string', description: 'Project name or "auto" (default: auto)' },
20
+ type: {
21
+ type: 'string',
22
+ enum: ['decision', 'discovery', 'pattern', 'failure', 'workaround', 'all'],
23
+ description: 'Filter by decision type (default: all)',
24
+ },
25
+ limit: { type: 'number', description: 'Max results (default: 10)' },
26
+ },
27
+ },
28
+ };
29
+ export async function handleGetDecisions(args) {
30
+ const topic = args.topic;
31
+ const typeFilter = args.type || 'all';
32
+ const limit = Math.min(Math.max(1, Number(args.limit) || 10), 50);
33
+ // Resolve project
34
+ let projectName;
35
+ const projectArg = args.project || 'auto';
36
+ if (projectArg === 'auto') {
37
+ const detected = await detectProject(process.cwd());
38
+ projectName = detected?.name;
39
+ }
40
+ else {
41
+ projectName = projectArg;
42
+ }
43
+ const data = await getDecisions({
44
+ topic,
45
+ project_name: projectName,
46
+ type: typeFilter,
47
+ limit,
48
+ });
49
+ session.recordToolCall();
50
+ if (!data.results || data.results.length === 0) {
51
+ const scope = projectName ? ` for ${projectName}` : '';
52
+ const topicNote = topic ? ` about "${topic}"` : '';
53
+ return `No decisions found${scope}${topicNote}. The decision log builds over time as agents auto-extract learnings from sessions.`;
54
+ }
55
+ const items = data.results.map((r, i) => {
56
+ const meta = r.metadata || {};
57
+ const typeLabel = meta.decision_type || meta.learning_type || 'unknown';
58
+ const confidence = meta.confidence ? ` (${meta.confidence} confidence)` : '';
59
+ // Special formatting for failures/anti-patterns
60
+ let prefix = '';
61
+ if (typeLabel === 'failure')
62
+ prefix = '[ANTI-PATTERN] ';
63
+ else if (typeLabel === 'architecture')
64
+ prefix = '[DECISION] ';
65
+ else if (typeLabel === 'pattern')
66
+ prefix = '[PATTERN] ';
67
+ else if (typeLabel === 'discovery')
68
+ prefix = '[DISCOVERY] ';
69
+ else if (typeLabel === 'workaround')
70
+ prefix = '[WORKAROUND] ';
71
+ let entry = `${i + 1}. ${prefix}**${r.title || 'Untitled'}**${confidence}\n`;
72
+ entry += ` ${r.insight || r.raw_content || '(no content)'}\n`;
73
+ if (meta.alternatives_considered) {
74
+ entry += ` *Alternatives considered:* ${meta.alternatives_considered}\n`;
75
+ }
76
+ if (meta.failed_reason) {
77
+ entry += ` *Why it failed:* ${meta.failed_reason}\n`;
78
+ }
79
+ if (meta.better_approach) {
80
+ entry += ` *Better approach:* ${meta.better_approach}\n`;
81
+ }
82
+ const tags = r.tags?.filter((t) => !t.startsWith('learning:') && t !== 'auto-extracted');
83
+ if (tags?.length > 0) {
84
+ entry += ` tags: ${tags.join(', ')}`;
85
+ }
86
+ return entry;
87
+ }).join('\n\n');
88
+ const scope = projectName ? ` for ${projectName}` : '';
89
+ return `*Decision log${scope} — ${data.results.length} result(s):*\n\n${items}`;
90
+ }
@@ -40,6 +40,12 @@ export async function handleGetProjectContext(args) {
40
40
  const results = data.results;
41
41
  const name = data.project_name || projectName;
42
42
  let output = '';
43
+ // --- SECTION 0: Active project patterns (cross-agent consistency) ---
44
+ const patterns = data.patterns;
45
+ if (patterns && patterns.length > 0) {
46
+ const patternItems = patterns.map((p, i) => `${i + 1}. **${p.name}** — ${p.description}`).join('\n');
47
+ output += `*Active patterns for ${name}:*\n${patternItems}\n\n---\n\n`;
48
+ }
43
49
  if (results && results.length > 0) {
44
50
  const items = results.map((r, i) => `${i + 1}. **${r.title || 'Untitled'}** · splicr [${r.relevance}]\n` +
45
51
  ` ${r.insight}\n` +
@@ -0,0 +1,14 @@
1
+ export declare const getTeamStatusSchema: {
2
+ name: "get_team_status";
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ project: {
8
+ type: "string";
9
+ description: string;
10
+ };
11
+ };
12
+ };
13
+ };
14
+ export declare function handleGetTeamStatus(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,61 @@
1
+ import { getTeamStatus } from '../lib/api-client.js';
2
+ import { detectProject } from '../lib/project-detector.js';
3
+ import * as session from '../lib/session-state.js';
4
+ export const getTeamStatusSchema = {
5
+ name: 'get_team_status',
6
+ description: `Get a brief on what the team is working on right now. Shows open PRs, active branches, and recent merges from GitHub.
7
+
8
+ Use when:
9
+ - Starting a session and want to know what's in flight
10
+ - Before starting work on a feature (check if someone else is already on it)
11
+ - To understand recent changes to the codebase
12
+
13
+ Requires GitHub integration (connected via Splicr dashboard).`,
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ project: { type: 'string', description: 'Project name or "auto" (default: auto)' },
18
+ },
19
+ },
20
+ };
21
+ export async function handleGetTeamStatus(args) {
22
+ const projectArg = args.project || 'auto';
23
+ let projectName;
24
+ if (projectArg === 'auto') {
25
+ const detected = await detectProject(process.cwd());
26
+ projectName = detected?.name;
27
+ }
28
+ else {
29
+ projectName = projectArg;
30
+ }
31
+ const data = await getTeamStatus({ project: projectName });
32
+ session.recordToolCall();
33
+ if (data.error) {
34
+ return data.error;
35
+ }
36
+ let output = `*Team status for ${data.repo}:*\n\n`;
37
+ // Open PRs
38
+ if (data.open_prs && data.open_prs.length > 0) {
39
+ output += `**Open PRs (${data.open_prs.length}):**\n`;
40
+ output += data.open_prs.map((pr) => `- #${pr.number} ${pr.title} (${pr.author}, branch: \`${pr.branch}\`)${pr.draft ? ' [DRAFT]' : ''}`).join('\n');
41
+ output += '\n\n';
42
+ }
43
+ else {
44
+ output += 'No open PRs.\n\n';
45
+ }
46
+ // Recent merges
47
+ if (data.recent_merges && data.recent_merges.length > 0) {
48
+ output += `**Recently merged (last 7 days):**\n`;
49
+ output += data.recent_merges.map((m) => {
50
+ const daysAgo = Math.floor((Date.now() - new Date(m.merged_at).getTime()) / (1000 * 60 * 60 * 24));
51
+ const when = daysAgo === 0 ? 'today' : daysAgo === 1 ? 'yesterday' : `${daysAgo}d ago`;
52
+ return `- ${m.title} (${m.author}, ${when})`;
53
+ }).join('\n');
54
+ output += '\n\n';
55
+ }
56
+ // Active branches
57
+ if (data.active_branches && data.active_branches.length > 0) {
58
+ output += `**Active branches:** ${data.active_branches.map((b) => `\`${b.name}\``).join(', ')}`;
59
+ }
60
+ return output.trim();
61
+ }
@@ -57,6 +57,7 @@ export async function handleGrepKnowledge(args) {
57
57
  session.addContextServed(data.results.map((r) => r.id).filter(Boolean));
58
58
  const formatted = data.results.map((r, i) => {
59
59
  const age = formatAge(r.created_at);
60
+ const teammate = r.from_teammate ? ' (from teammate)' : '';
60
61
  const projects = r.projects?.length > 0 ? ` -> ${r.projects.join(', ')}` : '';
61
62
  const clusterTag = r.cluster?.name
62
63
  ? `\n cluster: "${r.cluster.name}" (${r.cluster.member_count - 1} more)`
@@ -64,7 +65,7 @@ export async function handleGrepKnowledge(args) {
64
65
  const compiledTag = r.compiled_page
65
66
  ? `\n compiled: "${r.compiled_page.title}" - call get_compiled_page("${r.compiled_page.id}")`
66
67
  : '';
67
- return `${i + 1}. **${r.title || 'Untitled'}**\n` +
68
+ return `${i + 1}. **${r.title || 'Untitled'}**${teammate}\n` +
68
69
  ` ${r.insight ? r.insight.substring(0, 150) : 'No insight'}\n` +
69
70
  ` ${r.source_type || 'unknown'} | ${age} | tags: ${r.tags?.join(', ') || 'none'}${projects}` +
70
71
  clusterTag + compiledTag +
@@ -52,6 +52,7 @@ export declare const saveFromAgentSchema: {
52
52
  };
53
53
  decision_type: {
54
54
  type: "string";
55
+ enum: string[];
55
56
  description: string;
56
57
  };
57
58
  durability: {
@@ -60,6 +61,10 @@ export declare const saveFromAgentSchema: {
60
61
  };
61
62
  };
62
63
  };
64
+ register_as_pattern: {
65
+ type: "boolean";
66
+ description: string;
67
+ };
63
68
  };
64
69
  required: ("title" | "content")[];
65
70
  };
@@ -37,10 +37,14 @@ What NOT to save: file edits (git has those), routine commands, anything derivab
37
37
  agent: { type: 'string', description: 'Agent/model name (e.g. "claude-opus-4-6")' },
38
38
  files: { type: 'array', items: { type: 'string' }, description: 'Files involved in this learning' },
39
39
  splicr_refs: { type: 'array', items: { type: 'string' }, description: 'IDs of Splicr captures referenced during this session' },
40
- decision_type: { type: 'string', description: 'Type: architecture, workaround, discovery, synthesis, or pattern' },
40
+ decision_type: { type: 'string', enum: ['architecture', 'workaround', 'discovery', 'synthesis', 'pattern', 'failure'], description: 'Type: architecture, workaround, discovery, synthesis, pattern, or failure' },
41
41
  durability: { type: 'string', description: 'How long this knowledge stays relevant: permanent, versioned, or ephemeral' },
42
42
  },
43
43
  },
44
+ register_as_pattern: {
45
+ type: 'boolean',
46
+ description: 'Set true to also register this as an active project pattern (enforced across future agent sessions)',
47
+ },
44
48
  },
45
49
  required: ['content', 'title'],
46
50
  },
@@ -52,6 +56,7 @@ export async function handleSaveFromAgent(args) {
52
56
  const tags = args.tags || [];
53
57
  const memoryType = args.memory_type || 'fact';
54
58
  const context = args.context;
59
+ const registerAsPattern = args.register_as_pattern;
55
60
  // Resolve project name
56
61
  let projectName;
57
62
  if (projectArg === 'auto') {
@@ -68,6 +73,7 @@ export async function handleSaveFromAgent(args) {
68
73
  tags,
69
74
  memory_type: memoryType,
70
75
  context,
76
+ register_as_pattern: registerAsPattern,
71
77
  });
72
78
  if (result.duplicate) {
73
79
  return `*Already saved in Splicr:* "${title}"`;
@@ -105,6 +105,7 @@ function formatResults(results) {
105
105
  : 'n/a';
106
106
  const why = r.why_relevant ? `\n why: ${r.why_relevant}` : '';
107
107
  const crossTag = r.cross_project ? ' (cross-project)' : '';
108
+ const teammateTag = r.from_teammate ? ' (from teammate)' : '';
108
109
  const clusterTag = r.cluster?.name
109
110
  ? `\n cluster: "${r.cluster.name}" (${r.cluster.member_count - 1} more related saves)`
110
111
  : r.cluster?.member_count > 1
@@ -113,7 +114,7 @@ function formatResults(results) {
113
114
  const compiledTag = r.compiled_page
114
115
  ? `\n compiled: "${r.compiled_page.title}" — call get_compiled_page("${r.compiled_page.id}") for the synthesized wiki page`
115
116
  : '';
116
- return `${i + 1}. **${r.title || 'Untitled'}**${crossTag}\n` +
117
+ return `${i + 1}. **${r.title || 'Untitled'}**${teammateTag}${crossTag}\n` +
117
118
  ` ${r.insight}\n` +
118
119
  ` source: ${r.source_type || 'unknown'} | saved: ${age} | match: ${score} | tags: ${r.tags?.join(', ') || 'none'}${r.source_url ? `\n url: ${r.source_url}` : ''}` +
119
120
  why + clusterTag + compiledTag +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splicr/mcp-server",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "Splicr MCP server — route what you read to what you're building",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",