latitude-mcp-server 2.2.4 → 2.2.5

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/api.js +107 -73
  2. package/package.json +2 -1
package/dist/api.js CHANGED
@@ -31,6 +31,7 @@ exports.deployToLive = deployToLive;
31
31
  exports.getPromptNames = getPromptNames;
32
32
  const logger_util_js_1 = require("./utils/logger.util.js");
33
33
  const config_util_js_1 = require("./utils/config.util.js");
34
+ const promptl_ai_1 = require("promptl-ai");
34
35
  const logger = logger_util_js_1.Logger.forContext('api.ts');
35
36
  const DEFAULT_BASE_URL = 'https://gateway.latitude.so';
36
37
  const API_VERSION = 'v3';
@@ -398,84 +399,117 @@ async function runDocument(path, parameters, versionUuid = 'live') {
398
399
  });
399
400
  }
400
401
  /**
401
- * Pre-validate PromptL content locally to catch common issues before API call.
402
+ * Error code to human-readable fix suggestion mapping
403
+ */
404
+ const ERROR_SUGGESTIONS = {
405
+ 'message-tag-inside-message': {
406
+ rootCause: 'Message/role tags (<system>, <user>, <assistant>, <tool>) cannot be nested inside each other.',
407
+ suggestion: 'Move the nested tag outside its parent. If showing an example, use a code block (```yaml) instead of actual role tags.',
408
+ },
409
+ 'content-tag-inside-content': {
410
+ rootCause: 'Content tags (<text>, <image>, <file>, <tool-call>) must be directly inside message tags.',
411
+ suggestion: 'Restructure so content tags are direct children of message tags, not nested in other content.',
412
+ },
413
+ 'step-tag-inside-step': {
414
+ rootCause: 'Step/response tags cannot be nested inside each other.',
415
+ suggestion: 'Move the <response> tag outside its parent <response> tag.',
416
+ },
417
+ 'config-not-found': {
418
+ rootCause: 'PromptL files require a YAML configuration section at the top.',
419
+ suggestion: 'Add config at the beginning:\n---\nprovider: openai\nmodel: gpt-4\n---',
420
+ },
421
+ 'config-already-declared': {
422
+ rootCause: 'Only one configuration section is allowed per file.',
423
+ suggestion: 'Remove the duplicate --- config --- section.',
424
+ },
425
+ 'invalid-config': {
426
+ rootCause: 'The YAML configuration has syntax or validation errors.',
427
+ suggestion: 'Check YAML syntax. Required fields: model. Optional: provider, temperature, schema.',
428
+ },
429
+ 'unclosed-block': {
430
+ rootCause: 'A tag or block was opened but never closed.',
431
+ suggestion: 'Add the missing closing tag. Check for typos in tag names.',
432
+ },
433
+ 'unexpected-eof': {
434
+ rootCause: 'The file ended unexpectedly, likely due to unclosed tags or blocks.',
435
+ suggestion: 'Ensure all opened tags ({#if}, {#each}, <system>, etc.) are properly closed.',
436
+ },
437
+ 'variable-not-defined': {
438
+ rootCause: 'A variable is used but not provided in parameters.',
439
+ suggestion: 'Either pass this variable when calling the prompt, or define it with {#let}.',
440
+ },
441
+ 'invalid-tool-call-placement': {
442
+ rootCause: 'Tool calls (<tool-call>) can only appear inside <assistant> messages.',
443
+ suggestion: 'Move the <tool-call> tag inside an <assistant> block.',
444
+ },
445
+ };
446
+ /**
447
+ * Get snippet from content around a position
448
+ */
449
+ function getSnippet(content, startIndex, endIndex) {
450
+ const snippet = content.substring(startIndex, Math.min(endIndex, startIndex + 100));
451
+ return snippet.split('\n')[0] || snippet;
452
+ }
453
+ /**
454
+ * Pre-validate PromptL content using the official promptl-ai library.
402
455
  * Returns detailed, actionable error messages.
403
456
  */
404
- function validatePromptLContent(content, _path) {
457
+ async function validatePromptLContent(content, path) {
405
458
  const issues = [];
406
- const lines = content.split('\n');
407
- // Check for YAML frontmatter
408
- if (!content.startsWith('---')) {
409
- issues.push({
410
- type: 'error',
411
- message: 'Missing YAML frontmatter',
412
- rootCause: 'PromptL files must start with YAML frontmatter (---).',
413
- suggestion: 'Add frontmatter at the beginning:\n---\nprovider: YourProvider\nmodel: your-model\n---',
459
+ try {
460
+ // Use official promptl-ai scan function for validation
461
+ const result = await (0, promptl_ai_1.scan)({
462
+ prompt: content,
463
+ fullPath: path,
464
+ requireConfig: false, // Don't require config for flexibility
414
465
  });
415
- }
416
- // Check for nested role tags (common mistake)
417
- const roleStack = [];
418
- for (let i = 0; i < lines.length; i++) {
419
- const line = lines[i];
420
- const lineNum = i + 1;
421
- // Check for opening role tags
422
- const openMatches = line.matchAll(/<(system|user|assistant|tool)>/gi);
423
- for (const match of openMatches) {
424
- const tag = match[1].toLowerCase();
425
- if (roleStack.length > 0) {
426
- const parent = roleStack[roleStack.length - 1];
427
- issues.push({
428
- type: 'error',
429
- message: `Nested role tag: <${tag}> inside <${parent.tag}>`,
430
- rootCause: `Role tags (<system>, <user>, <assistant>, <tool>) cannot be nested. Found <${tag}> at line ${lineNum} inside <${parent.tag}> that started at line ${parent.line}.`,
431
- suggestion: `Move the <${tag}> block outside of <${parent.tag}>. Each role tag must be at the top level. If showing an example, use a code block (\`\`\`yaml) instead of actual role tags.`,
432
- lineNumber: lineNum,
433
- snippet: line.trim(),
434
- });
435
- }
436
- roleStack.push({ tag, line: lineNum });
437
- }
438
- // Check for closing role tags
439
- const closeMatches = line.matchAll(/<\/(system|user|assistant|tool)>/gi);
440
- for (const match of closeMatches) {
441
- const tag = match[1].toLowerCase();
442
- if (roleStack.length > 0 && roleStack[roleStack.length - 1].tag === tag) {
443
- roleStack.pop();
444
- }
466
+ // Convert CompileErrors to our ValidationIssue format
467
+ for (const compileError of result.errors) {
468
+ // CompileError has start?.line (Position) and startIndex (number)
469
+ const lineNumber = compileError.start?.line;
470
+ const snippet = getSnippet(content, compileError.startIndex, compileError.endIndex);
471
+ // Get human-readable suggestion based on error code
472
+ const suggestionInfo = ERROR_SUGGESTIONS[compileError.code] || {
473
+ rootCause: compileError.message,
474
+ suggestion: 'Review the PromptL documentation for correct syntax.',
475
+ };
476
+ issues.push({
477
+ type: 'error',
478
+ message: compileError.message,
479
+ rootCause: suggestionInfo.rootCause,
480
+ suggestion: suggestionInfo.suggestion,
481
+ lineNumber,
482
+ snippet,
483
+ });
445
484
  }
446
485
  }
447
- // Check for unclosed role tags
448
- for (const unclosed of roleStack) {
449
- issues.push({
450
- type: 'error',
451
- message: `Unclosed role tag: <${unclosed.tag}>`,
452
- rootCause: `The <${unclosed.tag}> tag opened at line ${unclosed.line} is never closed.`,
453
- suggestion: `Add </${unclosed.tag}> to close the tag.`,
454
- lineNumber: unclosed.line,
455
- });
456
- }
457
- // Check frontmatter has required fields
458
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
459
- if (frontmatterMatch) {
460
- const frontmatter = frontmatterMatch[1];
461
- if (!frontmatter.includes('model:') && !frontmatter.includes('model :')) {
486
+ catch (err) {
487
+ // Handle parse errors (thrown, not accumulated)
488
+ if (err instanceof promptl_ai_1.CompileError) {
489
+ const lineNumber = err.start?.line;
490
+ const snippet = getSnippet(content, err.startIndex, err.endIndex);
491
+ const suggestionInfo = ERROR_SUGGESTIONS[err.code] || {
492
+ rootCause: err.message,
493
+ suggestion: 'Fix the syntax error at the indicated location.',
494
+ };
462
495
  issues.push({
463
496
  type: 'error',
464
- message: 'Missing model in frontmatter',
465
- rootCause: 'PromptL requires a model to be specified in the frontmatter.',
466
- suggestion: 'Add model field:\n---\nmodel: gpt-4\n---\n\nOr with provider:\n---\nprovider: openai\nmodel: gpt-4\n---',
497
+ message: err.message,
498
+ rootCause: suggestionInfo.rootCause,
499
+ suggestion: suggestionInfo.suggestion,
500
+ lineNumber,
501
+ snippet,
502
+ });
503
+ }
504
+ else {
505
+ // Unknown error - still report it
506
+ issues.push({
507
+ type: 'error',
508
+ message: err instanceof Error ? err.message : 'Unknown validation error',
509
+ rootCause: 'An unexpected error occurred during validation.',
510
+ suggestion: 'Check the prompt content for syntax errors.',
467
511
  });
468
512
  }
469
- }
470
- // Check for empty content after frontmatter
471
- const contentAfterFrontmatter = content.replace(/^---[\s\S]*?---/, '').trim();
472
- if (!contentAfterFrontmatter) {
473
- issues.push({
474
- type: 'warning',
475
- message: 'Empty prompt content',
476
- rootCause: 'The prompt has no content after the frontmatter.',
477
- suggestion: 'Add prompt content using role tags:\n<system>\nYour system prompt here\n</system>',
478
- });
479
513
  }
480
514
  return issues;
481
515
  }
@@ -492,8 +526,8 @@ async function identifyFailingDocuments(changes) {
492
526
  for (const change of nonDeleteChanges) {
493
527
  if (!change.content)
494
528
  continue;
495
- const localIssues = validatePromptLContent(change.content, change.path);
496
- const errors = localIssues.filter(i => i.type === 'error');
529
+ const localIssues = await validatePromptLContent(change.content, change.path);
530
+ const errors = localIssues.filter((i) => i.type === 'error');
497
531
  if (errors.length > 0) {
498
532
  const mainError = errors[0];
499
533
  failed.push({
@@ -556,11 +590,11 @@ async function testSingleDocument(change) {
556
590
  }
557
591
  // Run local validation to give more context
558
592
  if (change.content) {
559
- const localIssues = validatePromptLContent(change.content, change.path);
593
+ const localIssues = await validatePromptLContent(change.content, change.path);
560
594
  if (localIssues.length > 0) {
561
- const warnings = localIssues.filter(i => i.type === 'warning');
595
+ const warnings = localIssues.filter((i) => i.type === 'warning');
562
596
  if (warnings.length > 0) {
563
- suggestion += `\n\nAdditional observations:\n${warnings.map(w => `- ${w.message}: ${w.suggestion}`).join('\n')}`;
597
+ suggestion += `\n\nAdditional observations:\n${warnings.map((w) => `- ${w.message}: ${w.suggestion}`).join('\n')}`;
564
598
  }
565
599
  }
566
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latitude-mcp-server",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
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",
@@ -74,6 +74,7 @@
74
74
  "dependencies": {
75
75
  "@modelcontextprotocol/sdk": "^1.23.0",
76
76
  "dotenv": "^17.2.3",
77
+ "promptl-ai": "^0.9.4",
77
78
  "zod": "^4.1.13"
78
79
  },
79
80
  "publishConfig": {