latitude-mcp-server 2.2.2 → 2.2.4
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 +261 -9
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -397,6 +397,222 @@ async function runDocument(path, parameters, versionUuid = 'live') {
|
|
|
397
397
|
timeout: API_TIMEOUT_MS,
|
|
398
398
|
});
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Pre-validate PromptL content locally to catch common issues before API call.
|
|
402
|
+
* Returns detailed, actionable error messages.
|
|
403
|
+
*/
|
|
404
|
+
function validatePromptLContent(content, _path) {
|
|
405
|
+
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---',
|
|
414
|
+
});
|
|
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
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
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 :')) {
|
|
462
|
+
issues.push({
|
|
463
|
+
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---',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
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
|
+
}
|
|
480
|
+
return issues;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Identify failing documents using binary search for efficiency.
|
|
484
|
+
* For N documents, worst case is O(N log N) API calls instead of O(N).
|
|
485
|
+
*/
|
|
486
|
+
async function identifyFailingDocuments(changes) {
|
|
487
|
+
const failed = [];
|
|
488
|
+
const nonDeleteChanges = changes.filter(c => c.status !== 'deleted');
|
|
489
|
+
if (nonDeleteChanges.length === 0)
|
|
490
|
+
return failed;
|
|
491
|
+
// First, run local validation to catch obvious issues without API calls
|
|
492
|
+
for (const change of nonDeleteChanges) {
|
|
493
|
+
if (!change.content)
|
|
494
|
+
continue;
|
|
495
|
+
const localIssues = validatePromptLContent(change.content, change.path);
|
|
496
|
+
const errors = localIssues.filter(i => i.type === 'error');
|
|
497
|
+
if (errors.length > 0) {
|
|
498
|
+
const mainError = errors[0];
|
|
499
|
+
failed.push({
|
|
500
|
+
path: change.path,
|
|
501
|
+
error: mainError.message,
|
|
502
|
+
rootCause: mainError.rootCause,
|
|
503
|
+
suggestion: mainError.suggestion,
|
|
504
|
+
lineNumber: mainError.lineNumber,
|
|
505
|
+
snippet: mainError.snippet,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// If we found local validation errors, return those first
|
|
510
|
+
if (failed.length > 0) {
|
|
511
|
+
logger.info(`Found ${failed.length} document(s) with local validation errors`);
|
|
512
|
+
return failed;
|
|
513
|
+
}
|
|
514
|
+
// If local validation passed, use binary search to find API-level failures
|
|
515
|
+
logger.info(`Local validation passed, testing ${nonDeleteChanges.length} document(s) against API...`);
|
|
516
|
+
// For small batches (<=5), test individually
|
|
517
|
+
if (nonDeleteChanges.length <= 5) {
|
|
518
|
+
return await testDocumentsIndividually(nonDeleteChanges);
|
|
519
|
+
}
|
|
520
|
+
// For larger batches, use binary search
|
|
521
|
+
return await binarySearchFailures(nonDeleteChanges);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Test documents individually (for small batches)
|
|
525
|
+
*/
|
|
526
|
+
async function testDocumentsIndividually(changes) {
|
|
527
|
+
const failed = [];
|
|
528
|
+
for (const change of changes) {
|
|
529
|
+
const result = await testSingleDocument(change);
|
|
530
|
+
if (result) {
|
|
531
|
+
failed.push(result);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return failed;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Test a single document and return failure details if it fails
|
|
538
|
+
*/
|
|
539
|
+
async function testSingleDocument(change) {
|
|
540
|
+
try {
|
|
541
|
+
const testDraft = await createVersion(`val-${Date.now()}-${change.path.slice(0, 20)}`);
|
|
542
|
+
await pushChanges(testDraft.uuid, [change]);
|
|
543
|
+
await publishVersion(testDraft.uuid);
|
|
544
|
+
return null; // Success
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
const errorMsg = error instanceof LatitudeApiError
|
|
548
|
+
? error.message
|
|
549
|
+
: (error instanceof Error ? error.message : 'Unknown error');
|
|
550
|
+
// Try to provide better root cause based on error patterns
|
|
551
|
+
let rootCause = 'The Latitude API rejected this document during publish validation.';
|
|
552
|
+
let suggestion = 'Review the document content for syntax errors or invalid configuration.';
|
|
553
|
+
if (errorMsg.includes('errors in the updated documents')) {
|
|
554
|
+
rootCause = 'The document has PromptL syntax or configuration errors that passed local validation but failed server-side validation.';
|
|
555
|
+
suggestion = 'Check for: 1) Invalid model/provider combination, 2) Malformed schema definition, 3) Invalid template syntax ({{ }}).';
|
|
556
|
+
}
|
|
557
|
+
// Run local validation to give more context
|
|
558
|
+
if (change.content) {
|
|
559
|
+
const localIssues = validatePromptLContent(change.content, change.path);
|
|
560
|
+
if (localIssues.length > 0) {
|
|
561
|
+
const warnings = localIssues.filter(i => i.type === 'warning');
|
|
562
|
+
if (warnings.length > 0) {
|
|
563
|
+
suggestion += `\n\nAdditional observations:\n${warnings.map(w => `- ${w.message}: ${w.suggestion}`).join('\n')}`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
path: change.path,
|
|
569
|
+
error: errorMsg,
|
|
570
|
+
rootCause,
|
|
571
|
+
suggestion,
|
|
572
|
+
snippet: change.content?.substring(0, 300),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Use binary search to efficiently find failures in large batches.
|
|
578
|
+
* Splits the batch in half, tests each half, and recurses on failing halves.
|
|
579
|
+
*/
|
|
580
|
+
async function binarySearchFailures(changes) {
|
|
581
|
+
// Base case: single document
|
|
582
|
+
if (changes.length === 1) {
|
|
583
|
+
const result = await testSingleDocument(changes[0]);
|
|
584
|
+
return result ? [result] : [];
|
|
585
|
+
}
|
|
586
|
+
// Test the entire batch first
|
|
587
|
+
const batchValid = await testBatch(changes);
|
|
588
|
+
if (batchValid) {
|
|
589
|
+
return []; // All documents in this batch are valid
|
|
590
|
+
}
|
|
591
|
+
// Batch has failures - split and recurse
|
|
592
|
+
const mid = Math.floor(changes.length / 2);
|
|
593
|
+
const left = changes.slice(0, mid);
|
|
594
|
+
const right = changes.slice(mid);
|
|
595
|
+
// Test both halves in parallel
|
|
596
|
+
const [leftFailures, rightFailures] = await Promise.all([
|
|
597
|
+
binarySearchFailures(left),
|
|
598
|
+
binarySearchFailures(right),
|
|
599
|
+
]);
|
|
600
|
+
return [...leftFailures, ...rightFailures];
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Test if a batch of documents can be published successfully
|
|
604
|
+
*/
|
|
605
|
+
async function testBatch(changes) {
|
|
606
|
+
try {
|
|
607
|
+
const testDraft = await createVersion(`batch-val-${Date.now()}`);
|
|
608
|
+
await pushChanges(testDraft.uuid, changes);
|
|
609
|
+
await publishVersion(testDraft.uuid);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
400
616
|
/**
|
|
401
617
|
* Create a synthetic Version object for no-op deploys.
|
|
402
618
|
* Returns a fully populated Version with all required fields.
|
|
@@ -461,15 +677,51 @@ async function deployToLive(changes, _versionName) {
|
|
|
461
677
|
logger.info(`Push complete: ${pushResult.documentsProcessed} documents processed`);
|
|
462
678
|
// Step 3: Publish the draft to make it LIVE
|
|
463
679
|
logger.info(`Publishing draft ${draft.uuid} to LIVE...`);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
680
|
+
try {
|
|
681
|
+
const published = await publishVersion(draft.uuid, draftName);
|
|
682
|
+
logger.info(`Published successfully! Version is now LIVE: ${published.uuid}`);
|
|
683
|
+
return {
|
|
684
|
+
version: published,
|
|
685
|
+
documentsProcessed: pushResult.documentsProcessed,
|
|
686
|
+
added,
|
|
687
|
+
modified,
|
|
688
|
+
deleted,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
catch (publishError) {
|
|
692
|
+
// Publish failed - identify which document(s) have validation errors
|
|
693
|
+
logger.warn('Batch publish failed, identifying problematic documents...');
|
|
694
|
+
const failedDocs = await identifyFailingDocuments(actualChanges);
|
|
695
|
+
if (failedDocs.length > 0) {
|
|
696
|
+
// Build detailed, actionable error message for LLM consumption
|
|
697
|
+
const errorLines = [];
|
|
698
|
+
for (const doc of failedDocs) {
|
|
699
|
+
errorLines.push(`\n## ❌ ${doc.path}`);
|
|
700
|
+
errorLines.push(`**Error:** ${doc.error}`);
|
|
701
|
+
errorLines.push(`**Root Cause:** ${doc.rootCause}`);
|
|
702
|
+
if (doc.lineNumber) {
|
|
703
|
+
errorLines.push(`**Location:** Line ${doc.lineNumber}`);
|
|
704
|
+
}
|
|
705
|
+
if (doc.snippet) {
|
|
706
|
+
errorLines.push(`**Snippet:** \`${doc.snippet}\``);
|
|
707
|
+
}
|
|
708
|
+
errorLines.push(`**Fix:** ${doc.suggestion}`);
|
|
709
|
+
}
|
|
710
|
+
const message = `${failedDocs.length} document(s) failed validation:${errorLines.join('\n')}`;
|
|
711
|
+
throw new LatitudeApiError({
|
|
712
|
+
name: 'DocumentValidationError',
|
|
713
|
+
errorCode: 'DOCUMENT_VALIDATION_FAILED',
|
|
714
|
+
message,
|
|
715
|
+
details: {
|
|
716
|
+
failedDocuments: failedDocs,
|
|
717
|
+
totalFailed: failedDocs.length,
|
|
718
|
+
failedPaths: failedDocs.map(d => d.path),
|
|
719
|
+
},
|
|
720
|
+
}, 422);
|
|
721
|
+
}
|
|
722
|
+
// Re-throw original error if we couldn't identify specific failures
|
|
723
|
+
throw publishError;
|
|
724
|
+
}
|
|
473
725
|
}
|
|
474
726
|
async function getPromptNames() {
|
|
475
727
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "latitude-mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
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",
|