latitude-mcp-server 3.0.1 → 3.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/dist/tools.d.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * MCP Tools for Latitude
3
3
  *
4
- * 8 Tools:
5
- * - list_prompts : List all prompt names in LIVE
6
- * - get_prompt : Get full prompt content by name
7
- * - run_prompt : Execute a prompt with parameters
8
- * - push_prompts : FULL SYNC to remote (adds, modifies, DELETES remote prompts not in local)
9
- * - pull_prompts : FULL SYNC from remote (deletes ALL local, downloads ALL from LIVE)
10
- * - append_prompts : ADDITIVE only (adds new, optionally updates existing, NEVER deletes)
11
- * - replace_prompt : Replace/create a single prompt (supports file path)
12
- * - docs : Documentation (help, get topic, find query)
4
+ * 7 Tools:
5
+ * - list_prompts : List all prompt names in LIVE
6
+ * - get_prompt : Get full prompt content by name
7
+ * - run_prompt : Execute a prompt with parameters (dynamic prompt list + variables)
8
+ * - push_prompts : FULL SYNC to remote (adds, modifies, DELETES remote prompts not in local)
9
+ * - pull_prompts : FULL SYNC from remote (deletes ALL local, downloads ALL from LIVE)
10
+ * - add_prompt : ADDITIVE - add/overwrite prompts without deleting others (dynamic prompt list)
11
+ * - docs : Documentation (help, get topic, find query)
13
12
  */
14
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
14
  export declare function registerTools(server: McpServer): Promise<void>;
package/dist/tools.js CHANGED
@@ -2,15 +2,14 @@
2
2
  /**
3
3
  * MCP Tools for Latitude
4
4
  *
5
- * 8 Tools:
6
- * - list_prompts : List all prompt names in LIVE
7
- * - get_prompt : Get full prompt content by name
8
- * - run_prompt : Execute a prompt with parameters
9
- * - push_prompts : FULL SYNC to remote (adds, modifies, DELETES remote prompts not in local)
10
- * - pull_prompts : FULL SYNC from remote (deletes ALL local, downloads ALL from LIVE)
11
- * - append_prompts : ADDITIVE only (adds new, optionally updates existing, NEVER deletes)
12
- * - replace_prompt : Replace/create a single prompt (supports file path)
13
- * - docs : Documentation (help, get topic, find query)
5
+ * 7 Tools:
6
+ * - list_prompts : List all prompt names in LIVE
7
+ * - get_prompt : Get full prompt content by name
8
+ * - run_prompt : Execute a prompt with parameters (dynamic prompt list + variables)
9
+ * - push_prompts : FULL SYNC to remote (adds, modifies, DELETES remote prompts not in local)
10
+ * - pull_prompts : FULL SYNC from remote (deletes ALL local, downloads ALL from LIVE)
11
+ * - add_prompt : ADDITIVE - add/overwrite prompts without deleting others (dynamic prompt list)
12
+ * - docs : Documentation (help, get topic, find query)
14
13
  */
15
14
  Object.defineProperty(exports, "__esModule", { value: true });
16
15
  exports.registerTools = registerTools;
@@ -106,6 +105,73 @@ function formatAvailablePrompts(names) {
106
105
  const formatted = names.map(n => `\`${n}\``).join(', ');
107
106
  return `\n\n**Available prompts (${names.length}):** ${formatted}`;
108
107
  }
108
+ /**
109
+ * Extract variable names from prompt content
110
+ * Looks for {{ variable }} and { variable } patterns
111
+ */
112
+ function extractVariables(content) {
113
+ const variables = new Set();
114
+ // Match {{ variable }} and { variable } patterns (PromptL syntax)
115
+ const patterns = [
116
+ /\{\{\s*(\w+)\s*\}\}/g, // {{ variable }}
117
+ /\{\s*(\w+)\s*\}/g, // { variable }
118
+ ];
119
+ for (const pattern of patterns) {
120
+ let match;
121
+ while ((match = pattern.exec(content)) !== null) {
122
+ // Exclude control flow keywords
123
+ const varName = match[1];
124
+ if (!['if', 'else', 'each', 'let', 'end', 'for', 'unless'].includes(varName)) {
125
+ variables.add(varName);
126
+ }
127
+ }
128
+ }
129
+ return Array.from(variables);
130
+ }
131
+ /**
132
+ * Build dynamic description for run_prompt with prompt names and their variables
133
+ */
134
+ async function buildRunPromptDescription() {
135
+ const names = await getCachedPromptNames();
136
+ let desc = 'Execute a prompt with parameters.';
137
+ if (names.length === 0) {
138
+ desc += '\n\n**No prompts in LIVE yet.**';
139
+ return desc;
140
+ }
141
+ desc += `\n\n**Available prompts (${names.length}):**`;
142
+ // Fetch each prompt to get its variables (limit to avoid too long description)
143
+ const maxToShow = Math.min(names.length, 10);
144
+ for (let i = 0; i < maxToShow; i++) {
145
+ try {
146
+ const doc = await (0, api_js_1.getDocument)(names[i], 'live');
147
+ const vars = extractVariables(doc.content);
148
+ if (vars.length > 0) {
149
+ desc += `\n- \`${names[i]}\` (params: ${vars.map(v => `\`${v}\``).join(', ')})`;
150
+ }
151
+ else {
152
+ desc += `\n- \`${names[i]}\` (no params)`;
153
+ }
154
+ }
155
+ catch {
156
+ desc += `\n- \`${names[i]}\``;
157
+ }
158
+ }
159
+ if (names.length > maxToShow) {
160
+ desc += `\n- ... and ${names.length - maxToShow} more`;
161
+ }
162
+ return desc;
163
+ }
164
+ /**
165
+ * Build dynamic description for add_prompt with available prompts
166
+ */
167
+ async function buildAddPromptDescription() {
168
+ const names = await getCachedPromptNames();
169
+ let desc = 'Add or update prompt(s) in LIVE without deleting others. ';
170
+ desc += 'If a prompt with the same name exists, it will be overwritten. ';
171
+ desc += 'Provide `prompts` array OR `filePaths` to .promptl files.';
172
+ desc += formatAvailablePrompts(names);
173
+ return desc;
174
+ }
109
175
  /**
110
176
  * Validate all prompts BEFORE pushing.
111
177
  * If ANY prompt fails validation, returns all errors and NOTHING is pushed.
@@ -303,25 +369,20 @@ async function handlePushPrompts(args) {
303
369
  return formatError(error);
304
370
  }
305
371
  }
306
- const AppendPromptsSchema = zod_1.z.object({
372
+ const AddPromptSchema = zod_1.z.object({
307
373
  prompts: zod_1.z
308
374
  .array(zod_1.z.object({
309
375
  name: zod_1.z.string().describe('Prompt name'),
310
376
  content: zod_1.z.string().describe('Prompt content'),
311
377
  }))
312
378
  .optional()
313
- .describe('Prompts to append - ADDITIVE: keeps existing prompts, adds new ones'),
379
+ .describe('Prompts to add/update - overwrites if exists, adds if new'),
314
380
  filePaths: zod_1.z
315
381
  .array(zod_1.z.string())
316
382
  .optional()
317
- .describe('File paths to .promptl files - ADDITIVE: never deletes existing prompts'),
318
- overwrite: zod_1.z
319
- .boolean()
320
- .optional()
321
- .default(false)
322
- .describe('If true, update existing prompts with same name (still no deletions)'),
383
+ .describe('File paths to .promptl files - overwrites if exists, adds if new'),
323
384
  });
324
- async function handleAppendPrompts(args) {
385
+ async function handleAddPrompt(args) {
325
386
  try {
326
387
  // Build prompts from either direct input or file paths
327
388
  let prompts = [];
@@ -337,9 +398,9 @@ async function handleAppendPrompts(args) {
337
398
  if (prompts.length === 0) {
338
399
  return formatError(new Error('No prompts provided. Use either prompts array or filePaths.'));
339
400
  }
340
- // PRE-VALIDATE ALL PROMPTS BEFORE APPENDING
341
- // If ANY prompt fails validation, return errors and append NOTHING
342
- logger.info(`Validating ${prompts.length} prompt(s) before append...`);
401
+ // PRE-VALIDATE ALL PROMPTS BEFORE ADDING
402
+ // If ANY prompt fails validation, return errors and add NOTHING
403
+ logger.info(`Validating ${prompts.length} prompt(s) before add...`);
343
404
  const validation = await validateAllPrompts(prompts);
344
405
  if (!validation.valid) {
345
406
  logger.warn(`Validation failed for ${validation.errors.length} prompt(s)`);
@@ -349,26 +410,20 @@ async function handleAppendPrompts(args) {
349
410
  // Get existing prompts
350
411
  const existingDocs = await (0, api_js_1.listDocuments)('live');
351
412
  const existingMap = new Map(existingDocs.map((d) => [d.path, d]));
352
- // Build changes - append does NOT delete existing prompts
413
+ // Build changes - ALWAYS overwrite if exists, add if new, NEVER delete
353
414
  const changes = [];
354
- const skipped = [];
355
415
  for (const prompt of prompts) {
356
416
  const existingDoc = existingMap.get(prompt.name);
357
417
  if (existingDoc) {
358
- if (args.overwrite) {
359
- // Only include if content is different
360
- if (existingDoc.content !== prompt.content) {
361
- changes.push({
362
- path: prompt.name,
363
- content: prompt.content,
364
- status: 'modified',
365
- });
366
- }
367
- // If same content, skip silently (unchanged)
368
- }
369
- else {
370
- skipped.push(prompt.name);
418
+ // Only include if content is different
419
+ if (existingDoc.content !== prompt.content) {
420
+ changes.push({
421
+ path: prompt.name,
422
+ content: prompt.content,
423
+ status: 'modified',
424
+ });
371
425
  }
426
+ // If same content, skip silently (unchanged)
372
427
  }
373
428
  else {
374
429
  // New prompt
@@ -382,28 +437,19 @@ async function handleAppendPrompts(args) {
382
437
  // Summarize
383
438
  const added = changes.filter((c) => c.status === 'added');
384
439
  const modified = changes.filter((c) => c.status === 'modified');
385
- if (changes.length === 0 && skipped.length === 0) {
440
+ if (changes.length === 0) {
386
441
  const newNames = await forceRefreshAndGetNames();
387
442
  return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
388
443
  `**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`);
389
444
  }
390
- if (changes.length === 0) {
391
- const newNames = await forceRefreshAndGetNames();
392
- let content = `**Skipped:** ${skipped.length} (already exist, use overwrite=true to update)\n`;
393
- content += `\n---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
394
- return formatSuccess('No Changes Made', content);
395
- }
396
445
  // Push all changes in one batch
397
446
  try {
398
- const result = await (0, api_js_1.deployToLive)(changes, 'append');
447
+ const result = await (0, api_js_1.deployToLive)(changes, 'add');
399
448
  // Force refresh cache after mutations
400
449
  const newNames = await forceRefreshAndGetNames();
401
450
  let content = `**Summary:**\n`;
402
451
  content += `- Added: ${added.length}\n`;
403
452
  content += `- Updated: ${modified.length}\n`;
404
- if (skipped.length > 0) {
405
- content += `- Skipped: ${skipped.length} (use overwrite=true)\n`;
406
- }
407
453
  content += `- Documents processed: ${result.documentsProcessed}\n\n`;
408
454
  if (added.length > 0) {
409
455
  content += `### Added\n${added.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
@@ -411,11 +457,8 @@ async function handleAppendPrompts(args) {
411
457
  if (modified.length > 0) {
412
458
  content += `### Updated\n${modified.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
413
459
  }
414
- if (skipped.length > 0) {
415
- content += `### Skipped (already exist)\n${skipped.map(n => `- \`${n}\``).join('\n')}\n\n`;
416
- }
417
460
  content += `---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
418
- return formatSuccess('Prompts Appended to LIVE', content);
461
+ return formatSuccess('Prompts Added to LIVE', content);
419
462
  }
420
463
  catch (error) {
421
464
  // Detailed error from API
@@ -472,93 +515,13 @@ async function handlePullPrompts(args) {
472
515
  content += `**Written:** ${written.length} file(s)\n\n`;
473
516
  content += `### Files\n\n`;
474
517
  content += written.map((f) => `- \`${f}\``).join('\n');
475
- content += `\n\n**Tip:** Edit files locally, then use \`replace_prompt\` with \`filePath\` to push changes.`;
518
+ content += `\n\n**Tip:** Edit files locally, then use \`add_prompt\` with \`filePaths\` to push changes.`;
476
519
  return formatSuccess('Prompts Pulled from LIVE', content);
477
520
  }
478
521
  catch (error) {
479
522
  return formatError(error);
480
523
  }
481
524
  }
482
- // Dynamic description builder for replace_prompt
483
- async function buildReplacePromptDescription() {
484
- const names = await getCachedPromptNames();
485
- let desc = 'Replace or create a single prompt in LIVE. ';
486
- desc += 'Provide either `content` directly or `filePath` to read from local file.';
487
- desc += formatAvailablePrompts(names);
488
- return desc;
489
- }
490
- const ReplacePromptSchema = zod_1.z.object({
491
- name: zod_1.z
492
- .string()
493
- .optional()
494
- .describe('Prompt name to replace (auto-detected from filePath if not provided)'),
495
- content: zod_1.z
496
- .string()
497
- .optional()
498
- .describe('New prompt content (alternative to filePath)'),
499
- filePath: zod_1.z
500
- .string()
501
- .optional()
502
- .describe('Path to .promptl file to read content from (alternative to content)'),
503
- });
504
- async function handleReplacePrompt(args) {
505
- try {
506
- let name = args.name;
507
- let content = args.content;
508
- // If filePath provided, read from file
509
- if (args.filePath) {
510
- const fileData = readPromptFile(args.filePath);
511
- content = fileData.content;
512
- // Use filename as name if not explicitly provided
513
- if (!name) {
514
- name = fileData.name;
515
- }
516
- }
517
- // Validate we have both name and content
518
- if (!name) {
519
- return formatError(new Error('Prompt name is required. Provide `name` or use `filePath` (name derived from filename).'));
520
- }
521
- if (!content) {
522
- return formatError(new Error('Prompt content is required. Provide either `content` or `filePath`.'));
523
- }
524
- // PRE-VALIDATE PROMPT BEFORE REPLACING
525
- // If validation fails, return error and replace NOTHING
526
- logger.info(`Validating prompt "${name}" before replace...`);
527
- const validation = await validateAllPrompts([{ name, content }]);
528
- if (!validation.valid) {
529
- logger.warn(`Validation failed for prompt "${name}"`);
530
- return formatValidationErrors(validation.errors);
531
- }
532
- logger.info(`Prompt "${name}" passed validation`);
533
- // Check if prompt exists
534
- const existingDocs = await (0, api_js_1.listDocuments)('live');
535
- const exists = existingDocs.some((d) => d.path === name);
536
- const changes = [
537
- {
538
- path: name,
539
- content: content,
540
- status: exists ? 'modified' : 'added',
541
- },
542
- ];
543
- // Deploy to LIVE (creates branch → pushes → publishes)
544
- const { version } = await (0, api_js_1.deployToLive)(changes, `replace ${name}`);
545
- // Force refresh cache after mutation
546
- const newNames = await forceRefreshAndGetNames();
547
- const action = exists ? 'Replaced' : 'Created';
548
- let result = `**Prompt:** \`${name}\`\n`;
549
- result += `**Action:** ${action}\n`;
550
- result += `**Version:** \`${version.uuid}\`\n`;
551
- if (args.filePath) {
552
- result += `**Source:** \`${args.filePath}\`\n`;
553
- }
554
- result += `\n### Content Preview\n\n\`\`\`promptl\n${content.substring(0, 500)}${content.length > 500 ? '...' : ''}\n\`\`\``;
555
- result += `\n\n---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
556
- return formatSuccess(`Prompt ${action}`, result);
557
- }
558
- catch (error) {
559
- return formatError(error);
560
- }
561
- }
562
525
  const DocsSchema = zod_1.z.object({
563
526
  action: zod_1.z
564
527
  .enum(['help', 'get', 'find'])
@@ -614,37 +577,34 @@ async function registerTools(server) {
614
577
  description: 'Get full prompt content by name',
615
578
  inputSchema: GetPromptSchema,
616
579
  }, handleGetPrompt);
580
+ // Build dynamic description with prompt names and their variables
581
+ const runDesc = await buildRunPromptDescription();
617
582
  server.registerTool('run_prompt', {
618
583
  title: 'Run Prompt',
619
- description: 'Execute a prompt with parameters',
584
+ description: runDesc,
620
585
  inputSchema: RunPromptSchema,
621
586
  }, handleRunPrompt);
622
587
  server.registerTool('push_prompts', {
623
- title: 'Push Prompts',
624
- description: 'Replace ALL prompts in LIVE. Provide `prompts` array OR `filePaths` to .promptl files. Creates branch pushes → publishes automatically.',
588
+ title: 'Push Prompts (FULL SYNC)',
589
+ description: 'FULL SYNC: Replace ALL prompts in LIVE. Deletes remote prompts not in your list. Use for initialization or complete sync.',
625
590
  inputSchema: PushPromptsSchema,
626
591
  }, handlePushPrompts);
627
- server.registerTool('append_prompts', {
628
- title: 'Append Prompts',
629
- description: 'Add prompts to LIVE without removing existing. Provide `prompts` array OR `filePaths`. Use overwrite=true to replace existing.',
630
- inputSchema: AppendPromptsSchema,
631
- }, handleAppendPrompts);
632
592
  server.registerTool('pull_prompts', {
633
- title: 'Pull Prompts',
634
- description: 'Download all prompts from LIVE to local ./prompts/*.promptl files',
593
+ title: 'Pull Prompts (FULL SYNC)',
594
+ description: 'FULL SYNC: Download all prompts from LIVE to local ./prompts/*.promptl files. Deletes existing local files first.',
635
595
  inputSchema: PullPromptsSchema,
636
596
  }, handlePullPrompts);
637
597
  // Build dynamic description with available prompts
638
- const replaceDesc = await buildReplacePromptDescription();
639
- server.registerTool('replace_prompt', {
640
- title: 'Replace Prompt',
641
- description: replaceDesc,
642
- inputSchema: ReplacePromptSchema,
643
- }, handleReplacePrompt);
598
+ const addDesc = await buildAddPromptDescription();
599
+ server.registerTool('add_prompt', {
600
+ title: 'Add/Update Prompt',
601
+ description: addDesc,
602
+ inputSchema: AddPromptSchema,
603
+ }, handleAddPrompt);
644
604
  server.registerTool('docs', {
645
605
  title: 'Documentation',
646
606
  description: 'Get documentation. Actions: help (overview), get (topic), find (search)',
647
607
  inputSchema: DocsSchema,
648
608
  }, handleDocs);
649
- logger.info(`Registered 8 MCP tools (${cachedPromptNames.length} prompts cached)`);
609
+ logger.info(`Registered 7 MCP tools (${cachedPromptNames.length} prompts cached)`);
650
610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latitude-mcp-server",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Simplified MCP server for Latitude.so prompt management - 8 focused tools for push, pull, run, and manage prompts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,71 @@
1
+ ---
2
+ provider: LiteLLM
3
+ model: claude-haiku-4-5
4
+ temperature: 0.7
5
+ schema:
6
+ type: object
7
+ properties:
8
+ cover_letter:
9
+ type: string
10
+ description: "The complete cover letter text"
11
+ key_points:
12
+ type: array
13
+ items:
14
+ type: string
15
+ description: "3-5 key points that make this candidate special for this role"
16
+ tone:
17
+ type: string
18
+ enum: [professional, enthusiastic, confident, conversational]
19
+ description: "The tone used in the letter"
20
+ required: [cover_letter, key_points, tone]
21
+ ---
22
+
23
+ <system>
24
+ # ROLE: The Ghostwriter - Elite Cover Letter Specialist
25
+
26
+ You write cover letters that get interviews. Your letters are:
27
+ - **Personal**: Reference specific career achievements
28
+ - **Targeted**: Connect experience directly to job requirements
29
+ - **Concise**: 250-350 words, no fluff
30
+ - **Unique**: Never generic, always tailored
31
+
32
+ ## FORMAT
33
+
34
+ ```
35
+ [Opening: Hook with relevant achievement]
36
+
37
+ [Body 1: Connect 2-3 career patterns to job requirements]
38
+
39
+ [Body 2: Address company/role specifically]
40
+
41
+ [Closing: Clear call to action]
42
+ ```
43
+
44
+ ## RULES
45
+
46
+ 1. **Use specific numbers** from patterns (years, percentages, team sizes)
47
+ 2. **Mirror job language** - use their keywords naturally
48
+ 3. **Show, don't tell** - achievements over adjectives
49
+ 4. **Address gaps proactively** if skills don't match
50
+ 5. **NO clichés**: "passionate", "team player", "hard worker", "excited to apply"
51
+
52
+ ## KEY POINTS SELECTION
53
+
54
+ Extract 3-5 points that:
55
+ - Directly match required skills
56
+ - Show quantifiable impact
57
+ - Demonstrate growth/leadership
58
+ - Are unique to this candidate
59
+ </system>
60
+
61
+ <user>
62
+ Write a cover letter for this job:
63
+
64
+ **Job Details:**
65
+ {{ job_details }}
66
+
67
+ **Candidate's Career Patterns:**
68
+ {{ career_patterns }}
69
+
70
+ Generate a personalized cover letter that connects their experience to this specific role at {{ company_name }}.
71
+ </user>