latitude-mcp-server 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * - POST /projects/:id/versions/:uuid/documents/create-or-update - create/update document
12
12
  * - POST /projects/:id/versions/:uuid/documents/run - run document
13
13
  */
14
- import type { LatitudeError, Version, Document, DocumentChange, DeployResult, RunResult } from './types.js';
14
+ import type { LatitudeError, Document, DocumentChange, DeployResult, RunResult } from './types.js';
15
15
  export declare class LatitudeApiError extends Error {
16
16
  readonly errorCode: string;
17
17
  readonly statusCode: number;
@@ -32,14 +32,8 @@ export declare class LatitudeApiError extends Error {
32
32
  * Get project ID from config
33
33
  */
34
34
  export declare function getProjectId(): string;
35
- export declare function listVersions(): Promise<Version[]>;
36
- export declare function getVersion(versionUuid: string): Promise<Version>;
37
- export declare function createVersion(name: string): Promise<Version>;
38
- export declare function publishVersion(versionUuid: string, title?: string): Promise<Version>;
39
35
  export declare function listDocuments(versionUuid?: string): Promise<Document[]>;
40
36
  export declare function getDocument(path: string, versionUuid?: string): Promise<Document>;
41
- export declare function createOrUpdateDocument(versionUuid: string, path: string, content: string, force?: boolean): Promise<Document>;
42
- export declare function deleteDocument(versionUuid: string, path: string): Promise<void>;
43
37
  /**
44
38
  * Push response from the API
45
39
  */
@@ -88,5 +82,4 @@ export declare function validatePromptLContent(content: string, path: string): P
88
82
  * This is the same workflow the CLI uses, ensuring proper validation.
89
83
  */
90
84
  export declare function deployToLive(changes: DocumentChange[], _versionName?: string): Promise<DeployResult>;
91
- export declare function getPromptNames(): Promise<string[]>;
92
85
  export {};
package/dist/api.js CHANGED
@@ -15,20 +15,13 @@
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.LatitudeApiError = void 0;
17
17
  exports.getProjectId = getProjectId;
18
- exports.listVersions = listVersions;
19
- exports.getVersion = getVersion;
20
- exports.createVersion = createVersion;
21
- exports.publishVersion = publishVersion;
22
18
  exports.listDocuments = listDocuments;
23
19
  exports.getDocument = getDocument;
24
- exports.createOrUpdateDocument = createOrUpdateDocument;
25
- exports.deleteDocument = deleteDocument;
26
20
  exports.pushChanges = pushChanges;
27
21
  exports.computeDiff = computeDiff;
28
22
  exports.runDocument = runDocument;
29
23
  exports.validatePromptLContent = validatePromptLContent;
30
24
  exports.deployToLive = deployToLive;
31
- exports.getPromptNames = getPromptNames;
32
25
  const logger_util_js_1 = require("./utils/logger.util.js");
33
26
  const config_util_js_1 = require("./utils/config.util.js");
34
27
  const promptl_ai_1 = require("promptl-ai");
@@ -266,14 +259,6 @@ exports.LatitudeApiError = LatitudeApiError;
266
259
  function getProjectId() {
267
260
  return getConfig().projectId;
268
261
  }
269
- async function listVersions() {
270
- const projectId = getProjectId();
271
- return request(`/projects/${projectId}/versions`);
272
- }
273
- async function getVersion(versionUuid) {
274
- const projectId = getProjectId();
275
- return request(`/projects/${projectId}/versions/${versionUuid}`);
276
- }
277
262
  async function createVersion(name) {
278
263
  const projectId = getProjectId();
279
264
  return request(`/projects/${projectId}/versions`, {
@@ -298,19 +283,6 @@ async function getDocument(path, versionUuid = 'live') {
298
283
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
299
284
  return request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`);
300
285
  }
301
- async function createOrUpdateDocument(versionUuid, path, content, force = false) {
302
- const projectId = getProjectId();
303
- logger.debug(`Creating/updating document: ${path} (${content.length} chars, force=${force})`);
304
- return request(`/projects/${projectId}/versions/${versionUuid}/documents/create-or-update`, {
305
- method: 'POST',
306
- body: { path, prompt: content, force },
307
- });
308
- }
309
- async function deleteDocument(versionUuid, path) {
310
- const projectId = getProjectId();
311
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
312
- await request(`/projects/${projectId}/versions/${versionUuid}/documents/${normalizedPath}`, { method: 'DELETE' });
313
- }
314
286
  /**
315
287
  * Push changes to a version in a single batch
316
288
  * This is the CLI-style push that sends all changes at once
@@ -740,13 +712,3 @@ async function deployToLive(changes, _versionName) {
740
712
  throw publishError;
741
713
  }
742
714
  }
743
- async function getPromptNames() {
744
- try {
745
- const docs = await listDocuments('live');
746
- return docs.map((doc) => doc.path);
747
- }
748
- catch (error) {
749
- logger.warn('Failed to fetch prompt names', error);
750
- return [];
751
- }
752
- }
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ async function main() {
20
20
  (0, server_js_1.setupGracefulShutdown)();
21
21
  // Log startup info
22
22
  logger.info('='.repeat(50));
23
- logger.info('Latitude MCP Server v2.0.0');
23
+ logger.info('Latitude MCP Server v3.1.0');
24
24
  logger.info('='.repeat(50));
25
25
  // Validate required environment variables
26
26
  const apiKey = config_util_js_1.config.get('LATITUDE_API_KEY');
package/dist/server.js CHANGED
@@ -17,7 +17,7 @@ const logger = logger_util_js_1.Logger.forContext('server.ts');
17
17
  // Server Configuration
18
18
  // ============================================================================
19
19
  const SERVER_NAME = 'latitude-mcp-server';
20
- const SERVER_VERSION = '2.0.0';
20
+ const SERVER_VERSION = '3.1.0';
21
21
  // ============================================================================
22
22
  // Server Instance
23
23
  // ============================================================================
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.
@@ -226,6 +292,10 @@ const PushPromptsSchema = zod_1.z.object({
226
292
  .array(zod_1.z.string())
227
293
  .optional()
228
294
  .describe('File paths to .promptl files - FULL SYNC: deletes remote prompts not in this list'),
295
+ versionName: zod_1.z
296
+ .string()
297
+ .optional()
298
+ .describe('Optional version name (like git branch: feat/add-auth, fix/typo). If omitted, auto-generates timestamp.'),
229
299
  });
230
300
  async function handlePushPrompts(args) {
231
301
  try {
@@ -268,7 +338,7 @@ async function handlePushPrompts(args) {
268
338
  }
269
339
  // Push all changes in one batch
270
340
  try {
271
- const result = await (0, api_js_1.deployToLive)(changes, 'push');
341
+ const result = await (0, api_js_1.deployToLive)(changes, args.versionName || 'push');
272
342
  // Force refresh cache after mutations
273
343
  const newNames = await forceRefreshAndGetNames();
274
344
  let content = `**Summary:**\n`;
@@ -303,25 +373,24 @@ async function handlePushPrompts(args) {
303
373
  return formatError(error);
304
374
  }
305
375
  }
306
- const AppendPromptsSchema = zod_1.z.object({
376
+ const AddPromptSchema = zod_1.z.object({
307
377
  prompts: zod_1.z
308
378
  .array(zod_1.z.object({
309
379
  name: zod_1.z.string().describe('Prompt name'),
310
380
  content: zod_1.z.string().describe('Prompt content'),
311
381
  }))
312
382
  .optional()
313
- .describe('Prompts to append - ADDITIVE: keeps existing prompts, adds new ones'),
383
+ .describe('Prompts to add/update - overwrites if exists, adds if new'),
314
384
  filePaths: zod_1.z
315
385
  .array(zod_1.z.string())
316
386
  .optional()
317
- .describe('File paths to .promptl files - ADDITIVE: never deletes existing prompts'),
318
- overwrite: zod_1.z
319
- .boolean()
387
+ .describe('File paths to .promptl files - overwrites if exists, adds if new'),
388
+ versionName: zod_1.z
389
+ .string()
320
390
  .optional()
321
- .default(false)
322
- .describe('If true, update existing prompts with same name (still no deletions)'),
391
+ .describe('Optional version name (like git commit: feat/add-auth, fix/typo). Describes what changed.'),
323
392
  });
324
- async function handleAppendPrompts(args) {
393
+ async function handleAddPrompt(args) {
325
394
  try {
326
395
  // Build prompts from either direct input or file paths
327
396
  let prompts = [];
@@ -337,9 +406,9 @@ async function handleAppendPrompts(args) {
337
406
  if (prompts.length === 0) {
338
407
  return formatError(new Error('No prompts provided. Use either prompts array or filePaths.'));
339
408
  }
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...`);
409
+ // PRE-VALIDATE ALL PROMPTS BEFORE ADDING
410
+ // If ANY prompt fails validation, return errors and add NOTHING
411
+ logger.info(`Validating ${prompts.length} prompt(s) before add...`);
343
412
  const validation = await validateAllPrompts(prompts);
344
413
  if (!validation.valid) {
345
414
  logger.warn(`Validation failed for ${validation.errors.length} prompt(s)`);
@@ -349,26 +418,20 @@ async function handleAppendPrompts(args) {
349
418
  // Get existing prompts
350
419
  const existingDocs = await (0, api_js_1.listDocuments)('live');
351
420
  const existingMap = new Map(existingDocs.map((d) => [d.path, d]));
352
- // Build changes - append does NOT delete existing prompts
421
+ // Build changes - ALWAYS overwrite if exists, add if new, NEVER delete
353
422
  const changes = [];
354
- const skipped = [];
355
423
  for (const prompt of prompts) {
356
424
  const existingDoc = existingMap.get(prompt.name);
357
425
  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);
426
+ // Only include if content is different
427
+ if (existingDoc.content !== prompt.content) {
428
+ changes.push({
429
+ path: prompt.name,
430
+ content: prompt.content,
431
+ status: 'modified',
432
+ });
371
433
  }
434
+ // If same content, skip silently (unchanged)
372
435
  }
373
436
  else {
374
437
  // New prompt
@@ -382,28 +445,19 @@ async function handleAppendPrompts(args) {
382
445
  // Summarize
383
446
  const added = changes.filter((c) => c.status === 'added');
384
447
  const modified = changes.filter((c) => c.status === 'modified');
385
- if (changes.length === 0 && skipped.length === 0) {
448
+ if (changes.length === 0) {
386
449
  const newNames = await forceRefreshAndGetNames();
387
450
  return formatSuccess('No Changes Needed', `All ${prompts.length} prompt(s) are already up to date.\n\n` +
388
451
  `**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`);
389
452
  }
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
453
  // Push all changes in one batch
397
454
  try {
398
- const result = await (0, api_js_1.deployToLive)(changes, 'append');
455
+ const result = await (0, api_js_1.deployToLive)(changes, args.versionName || 'add');
399
456
  // Force refresh cache after mutations
400
457
  const newNames = await forceRefreshAndGetNames();
401
458
  let content = `**Summary:**\n`;
402
459
  content += `- Added: ${added.length}\n`;
403
460
  content += `- Updated: ${modified.length}\n`;
404
- if (skipped.length > 0) {
405
- content += `- Skipped: ${skipped.length} (use overwrite=true)\n`;
406
- }
407
461
  content += `- Documents processed: ${result.documentsProcessed}\n\n`;
408
462
  if (added.length > 0) {
409
463
  content += `### Added\n${added.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
@@ -411,11 +465,8 @@ async function handleAppendPrompts(args) {
411
465
  if (modified.length > 0) {
412
466
  content += `### Updated\n${modified.map((c) => `- \`${c.path}\``).join('\n')}\n\n`;
413
467
  }
414
- if (skipped.length > 0) {
415
- content += `### Skipped (already exist)\n${skipped.map(n => `- \`${n}\``).join('\n')}\n\n`;
416
- }
417
468
  content += `---\n**Current LIVE prompts (${newNames.length}):** ${newNames.map(n => `\`${n}\``).join(', ')}`;
418
- return formatSuccess('Prompts Appended to LIVE', content);
469
+ return formatSuccess('Prompts Added to LIVE', content);
419
470
  }
420
471
  catch (error) {
421
472
  // Detailed error from API
@@ -472,93 +523,13 @@ async function handlePullPrompts(args) {
472
523
  content += `**Written:** ${written.length} file(s)\n\n`;
473
524
  content += `### Files\n\n`;
474
525
  content += written.map((f) => `- \`${f}\``).join('\n');
475
- content += `\n\n**Tip:** Edit files locally, then use \`replace_prompt\` with \`filePath\` to push changes.`;
526
+ content += `\n\n**Tip:** Edit files locally, then use \`add_prompt\` with \`filePaths\` to push changes.`;
476
527
  return formatSuccess('Prompts Pulled from LIVE', content);
477
528
  }
478
529
  catch (error) {
479
530
  return formatError(error);
480
531
  }
481
532
  }
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
533
  const DocsSchema = zod_1.z.object({
563
534
  action: zod_1.z
564
535
  .enum(['help', 'get', 'find'])
@@ -614,37 +585,34 @@ async function registerTools(server) {
614
585
  description: 'Get full prompt content by name',
615
586
  inputSchema: GetPromptSchema,
616
587
  }, handleGetPrompt);
588
+ // Build dynamic description with prompt names and their variables
589
+ const runDesc = await buildRunPromptDescription();
617
590
  server.registerTool('run_prompt', {
618
591
  title: 'Run Prompt',
619
- description: 'Execute a prompt with parameters',
592
+ description: runDesc,
620
593
  inputSchema: RunPromptSchema,
621
594
  }, handleRunPrompt);
622
595
  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.',
596
+ title: 'Push Prompts (FULL SYNC)',
597
+ description: 'FULL SYNC: Replace ALL prompts in LIVE. Deletes remote prompts not in your list. Use for initialization or complete sync.',
625
598
  inputSchema: PushPromptsSchema,
626
599
  }, 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
600
  server.registerTool('pull_prompts', {
633
- title: 'Pull Prompts',
634
- description: 'Download all prompts from LIVE to local ./prompts/*.promptl files',
601
+ title: 'Pull Prompts (FULL SYNC)',
602
+ description: 'FULL SYNC: Download all prompts from LIVE to local ./prompts/*.promptl files. Deletes existing local files first.',
635
603
  inputSchema: PullPromptsSchema,
636
604
  }, handlePullPrompts);
637
605
  // 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);
606
+ const addDesc = await buildAddPromptDescription();
607
+ server.registerTool('add_prompt', {
608
+ title: 'Add/Update Prompt',
609
+ description: addDesc,
610
+ inputSchema: AddPromptSchema,
611
+ }, handleAddPrompt);
644
612
  server.registerTool('docs', {
645
613
  title: 'Documentation',
646
614
  description: 'Get documentation. Actions: help (overview), get (topic), find (search)',
647
615
  inputSchema: DocsSchema,
648
616
  }, handleDocs);
649
- logger.info(`Registered 8 MCP tools (${cachedPromptNames.length} prompts cached)`);
617
+ logger.info(`Registered 7 MCP tools (${cachedPromptNames.length} prompts cached)`);
650
618
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latitude-mcp-server",
3
- "version": "3.0.1",
3
+ "version": "3.2.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>