latitude-mcp-server 2.2.3 → 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.
- package/dist/api.js +260 -26
- 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,39 +399,254 @@ async function runDocument(path, parameters, versionUuid = 'live') {
|
|
|
398
399
|
});
|
|
399
400
|
}
|
|
400
401
|
/**
|
|
401
|
-
*
|
|
402
|
-
|
|
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.
|
|
455
|
+
* Returns detailed, actionable error messages.
|
|
456
|
+
*/
|
|
457
|
+
async function validatePromptLContent(content, path) {
|
|
458
|
+
const issues = [];
|
|
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
|
|
465
|
+
});
|
|
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
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
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
|
+
};
|
|
495
|
+
issues.push({
|
|
496
|
+
type: 'error',
|
|
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.',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return issues;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Identify failing documents using binary search for efficiency.
|
|
518
|
+
* For N documents, worst case is O(N log N) API calls instead of O(N).
|
|
403
519
|
*/
|
|
404
520
|
async function identifyFailingDocuments(changes) {
|
|
405
521
|
const failed = [];
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
logger.debug(`Document ${change.path} validated successfully`);
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
// This document failed - capture the error
|
|
421
|
-
const errorMsg = error instanceof LatitudeApiError
|
|
422
|
-
? error.message
|
|
423
|
-
: (error instanceof Error ? error.message : 'Unknown validation error');
|
|
522
|
+
const nonDeleteChanges = changes.filter(c => c.status !== 'deleted');
|
|
523
|
+
if (nonDeleteChanges.length === 0)
|
|
524
|
+
return failed;
|
|
525
|
+
// First, run local validation to catch obvious issues without API calls
|
|
526
|
+
for (const change of nonDeleteChanges) {
|
|
527
|
+
if (!change.content)
|
|
528
|
+
continue;
|
|
529
|
+
const localIssues = await validatePromptLContent(change.content, change.path);
|
|
530
|
+
const errors = localIssues.filter((i) => i.type === 'error');
|
|
531
|
+
if (errors.length > 0) {
|
|
532
|
+
const mainError = errors[0];
|
|
424
533
|
failed.push({
|
|
425
534
|
path: change.path,
|
|
426
|
-
error:
|
|
427
|
-
|
|
535
|
+
error: mainError.message,
|
|
536
|
+
rootCause: mainError.rootCause,
|
|
537
|
+
suggestion: mainError.suggestion,
|
|
538
|
+
lineNumber: mainError.lineNumber,
|
|
539
|
+
snippet: mainError.snippet,
|
|
428
540
|
});
|
|
429
|
-
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// If we found local validation errors, return those first
|
|
544
|
+
if (failed.length > 0) {
|
|
545
|
+
logger.info(`Found ${failed.length} document(s) with local validation errors`);
|
|
546
|
+
return failed;
|
|
547
|
+
}
|
|
548
|
+
// If local validation passed, use binary search to find API-level failures
|
|
549
|
+
logger.info(`Local validation passed, testing ${nonDeleteChanges.length} document(s) against API...`);
|
|
550
|
+
// For small batches (<=5), test individually
|
|
551
|
+
if (nonDeleteChanges.length <= 5) {
|
|
552
|
+
return await testDocumentsIndividually(nonDeleteChanges);
|
|
553
|
+
}
|
|
554
|
+
// For larger batches, use binary search
|
|
555
|
+
return await binarySearchFailures(nonDeleteChanges);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Test documents individually (for small batches)
|
|
559
|
+
*/
|
|
560
|
+
async function testDocumentsIndividually(changes) {
|
|
561
|
+
const failed = [];
|
|
562
|
+
for (const change of changes) {
|
|
563
|
+
const result = await testSingleDocument(change);
|
|
564
|
+
if (result) {
|
|
565
|
+
failed.push(result);
|
|
430
566
|
}
|
|
431
567
|
}
|
|
432
568
|
return failed;
|
|
433
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Test a single document and return failure details if it fails
|
|
572
|
+
*/
|
|
573
|
+
async function testSingleDocument(change) {
|
|
574
|
+
try {
|
|
575
|
+
const testDraft = await createVersion(`val-${Date.now()}-${change.path.slice(0, 20)}`);
|
|
576
|
+
await pushChanges(testDraft.uuid, [change]);
|
|
577
|
+
await publishVersion(testDraft.uuid);
|
|
578
|
+
return null; // Success
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
const errorMsg = error instanceof LatitudeApiError
|
|
582
|
+
? error.message
|
|
583
|
+
: (error instanceof Error ? error.message : 'Unknown error');
|
|
584
|
+
// Try to provide better root cause based on error patterns
|
|
585
|
+
let rootCause = 'The Latitude API rejected this document during publish validation.';
|
|
586
|
+
let suggestion = 'Review the document content for syntax errors or invalid configuration.';
|
|
587
|
+
if (errorMsg.includes('errors in the updated documents')) {
|
|
588
|
+
rootCause = 'The document has PromptL syntax or configuration errors that passed local validation but failed server-side validation.';
|
|
589
|
+
suggestion = 'Check for: 1) Invalid model/provider combination, 2) Malformed schema definition, 3) Invalid template syntax ({{ }}).';
|
|
590
|
+
}
|
|
591
|
+
// Run local validation to give more context
|
|
592
|
+
if (change.content) {
|
|
593
|
+
const localIssues = await validatePromptLContent(change.content, change.path);
|
|
594
|
+
if (localIssues.length > 0) {
|
|
595
|
+
const warnings = localIssues.filter((i) => i.type === 'warning');
|
|
596
|
+
if (warnings.length > 0) {
|
|
597
|
+
suggestion += `\n\nAdditional observations:\n${warnings.map((w) => `- ${w.message}: ${w.suggestion}`).join('\n')}`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
path: change.path,
|
|
603
|
+
error: errorMsg,
|
|
604
|
+
rootCause,
|
|
605
|
+
suggestion,
|
|
606
|
+
snippet: change.content?.substring(0, 300),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Use binary search to efficiently find failures in large batches.
|
|
612
|
+
* Splits the batch in half, tests each half, and recurses on failing halves.
|
|
613
|
+
*/
|
|
614
|
+
async function binarySearchFailures(changes) {
|
|
615
|
+
// Base case: single document
|
|
616
|
+
if (changes.length === 1) {
|
|
617
|
+
const result = await testSingleDocument(changes[0]);
|
|
618
|
+
return result ? [result] : [];
|
|
619
|
+
}
|
|
620
|
+
// Test the entire batch first
|
|
621
|
+
const batchValid = await testBatch(changes);
|
|
622
|
+
if (batchValid) {
|
|
623
|
+
return []; // All documents in this batch are valid
|
|
624
|
+
}
|
|
625
|
+
// Batch has failures - split and recurse
|
|
626
|
+
const mid = Math.floor(changes.length / 2);
|
|
627
|
+
const left = changes.slice(0, mid);
|
|
628
|
+
const right = changes.slice(mid);
|
|
629
|
+
// Test both halves in parallel
|
|
630
|
+
const [leftFailures, rightFailures] = await Promise.all([
|
|
631
|
+
binarySearchFailures(left),
|
|
632
|
+
binarySearchFailures(right),
|
|
633
|
+
]);
|
|
634
|
+
return [...leftFailures, ...rightFailures];
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Test if a batch of documents can be published successfully
|
|
638
|
+
*/
|
|
639
|
+
async function testBatch(changes) {
|
|
640
|
+
try {
|
|
641
|
+
const testDraft = await createVersion(`batch-val-${Date.now()}`);
|
|
642
|
+
await pushChanges(testDraft.uuid, changes);
|
|
643
|
+
await publishVersion(testDraft.uuid);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
434
650
|
/**
|
|
435
651
|
* Create a synthetic Version object for no-op deploys.
|
|
436
652
|
* Returns a fully populated Version with all required fields.
|
|
@@ -511,12 +727,30 @@ async function deployToLive(changes, _versionName) {
|
|
|
511
727
|
logger.warn('Batch publish failed, identifying problematic documents...');
|
|
512
728
|
const failedDocs = await identifyFailingDocuments(actualChanges);
|
|
513
729
|
if (failedDocs.length > 0) {
|
|
514
|
-
|
|
730
|
+
// Build detailed, actionable error message for LLM consumption
|
|
731
|
+
const errorLines = [];
|
|
732
|
+
for (const doc of failedDocs) {
|
|
733
|
+
errorLines.push(`\n## ❌ ${doc.path}`);
|
|
734
|
+
errorLines.push(`**Error:** ${doc.error}`);
|
|
735
|
+
errorLines.push(`**Root Cause:** ${doc.rootCause}`);
|
|
736
|
+
if (doc.lineNumber) {
|
|
737
|
+
errorLines.push(`**Location:** Line ${doc.lineNumber}`);
|
|
738
|
+
}
|
|
739
|
+
if (doc.snippet) {
|
|
740
|
+
errorLines.push(`**Snippet:** \`${doc.snippet}\``);
|
|
741
|
+
}
|
|
742
|
+
errorLines.push(`**Fix:** ${doc.suggestion}`);
|
|
743
|
+
}
|
|
744
|
+
const message = `${failedDocs.length} document(s) failed validation:${errorLines.join('\n')}`;
|
|
515
745
|
throw new LatitudeApiError({
|
|
516
746
|
name: 'DocumentValidationError',
|
|
517
747
|
errorCode: 'DOCUMENT_VALIDATION_FAILED',
|
|
518
|
-
message
|
|
519
|
-
details: {
|
|
748
|
+
message,
|
|
749
|
+
details: {
|
|
750
|
+
failedDocuments: failedDocs,
|
|
751
|
+
totalFailed: failedDocs.length,
|
|
752
|
+
failedPaths: failedDocs.map(d => d.path),
|
|
753
|
+
},
|
|
520
754
|
}, 422);
|
|
521
755
|
}
|
|
522
756
|
// Re-throw original error if we couldn't identify specific failures
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latitude-mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
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": {
|