lptp-mcp-server 0.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.
Files changed (42) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +91 -0
  3. package/build/backends/gprolog.d.ts +32 -0
  4. package/build/backends/gprolog.d.ts.map +1 -0
  5. package/build/backends/gprolog.js +113 -0
  6. package/build/backends/gprolog.js.map +1 -0
  7. package/build/backends/registry.d.ts +15 -0
  8. package/build/backends/registry.d.ts.map +1 -0
  9. package/build/backends/registry.js +61 -0
  10. package/build/backends/registry.js.map +1 -0
  11. package/build/backends/scryer.d.ts +33 -0
  12. package/build/backends/scryer.d.ts.map +1 -0
  13. package/build/backends/scryer.js +71 -0
  14. package/build/backends/scryer.js.map +1 -0
  15. package/build/backends/swipl.d.ts +24 -0
  16. package/build/backends/swipl.d.ts.map +1 -0
  17. package/build/backends/swipl.js +73 -0
  18. package/build/backends/swipl.js.map +1 -0
  19. package/build/backends/types.d.ts +27 -0
  20. package/build/backends/types.d.ts.map +1 -0
  21. package/build/backends/types.js +3 -0
  22. package/build/backends/types.js.map +1 -0
  23. package/build/index.d.ts +3 -0
  24. package/build/index.d.ts.map +1 -0
  25. package/build/index.js +355 -0
  26. package/build/index.js.map +1 -0
  27. package/build/lptpExecutor.d.ts +19 -0
  28. package/build/lptpExecutor.d.ts.map +1 -0
  29. package/build/lptpExecutor.js +520 -0
  30. package/build/lptpExecutor.js.map +1 -0
  31. package/jest.config.js +11 -0
  32. package/mcp-config-example.json +12 -0
  33. package/package.json +41 -0
  34. package/src/__tests__/mcp_lptp_server.test.ts +97 -0
  35. package/src/backends/gprolog.ts +87 -0
  36. package/src/backends/registry.ts +70 -0
  37. package/src/backends/scryer.ts +83 -0
  38. package/src/backends/swipl.ts +88 -0
  39. package/src/backends/types.ts +36 -0
  40. package/src/index.ts +369 -0
  41. package/src/lptpExecutor.ts +551 -0
  42. package/tsconfig.json +26 -0
@@ -0,0 +1,97 @@
1
+ import { applyTactic, verifyLemma, checkProof } from '../lptpExecutor';
2
+ import * as path from 'path';
3
+
4
+ describe('LPTP Tactic Applications', () => {
5
+ // These tests currently fail since we haven't implemented the functions yet!
6
+ // Strict TDD: Write the test, verify failure, then write the implementation.
7
+
8
+ describe('verifyLemma', () => {
9
+ it('should correctly verify a grammatically sound lemma without tactics', async () => {
10
+ const proof = "succeeds member(?y, [?y|?l]) by completion";
11
+ const result = await verifyLemma(proof);
12
+ expect(result.success).toBe(true);
13
+ expect(result.output).toContain('parsed and verified successfully');
14
+ });
15
+ });
16
+
17
+ describe('checkProof', () => {
18
+ it('should correctly execute the check/1 predicate against an existing file', async () => {
19
+ // Resolve relative to LPTP_ROOT_DIR or walk up from test location
20
+ const lptpRoot = process.env['LPTP_ROOT_DIR'] || path.resolve(__dirname, '..', '..', '..', '..', '..');
21
+ const mockFilePath = path.join(lptpRoot, 'test', 'proof', 'proof1.pr');
22
+ const result = await checkProof(mockFilePath);
23
+ // When SWI-Prolog is present, success depends on proof validity.
24
+ // We only assert the call completes and returns a string output.
25
+ expect(result.output).toBeDefined();
26
+ expect(typeof result.output).toBe('string');
27
+ });
28
+ });
29
+
30
+ describe('applyTactic', () => {
31
+ // Test definitions for the exhaustive tactic list required
32
+
33
+ it('handles the `auto(N)` tactic', async () => {
34
+ const formula = "sub([?y|?l1], ?l2)";
35
+ const result = await applyTactic(formula, "[auto(11)]", "all [l1,l2]: sub(?l1,?l2) <=> (all x: succeeds member(?x,?l1) => succeeds member(?x,?l2))");
36
+ expect(result.success).toBe(true);
37
+ expect(result.derivation).toBeDefined();
38
+ });
39
+
40
+ it('handles the `ind` tactic', async () => {
41
+ const formula = "all [y,l1,l2]: succeeds append(?l1,?l2,?l3) => succeeds member(?y,?l1) \\/ succeeds member(?y,?l2)";
42
+ const result = await applyTactic(formula, "[ind]");
43
+ expect(result.success).toBe(true);
44
+ expect(result.derivation).toContain('assume(');
45
+ });
46
+
47
+ it('handles the `case` tactic', async () => {
48
+ const formula = "succeeds member(?x, ?l1) \\/ succeeds member(?x, ?l2)";
49
+ const result = await applyTactic(formula, "[case]");
50
+ expect(result.success).toBe(true);
51
+ expect(result.derivation).toContain('by gap');
52
+ });
53
+
54
+ it('handles the `elim` tactic', async () => {
55
+ const formula = "succeeds member(?x, [?y|?l])";
56
+ const result = await applyTactic(formula, "[elim]");
57
+ expect(result.success).toBe(true);
58
+ });
59
+
60
+ it('handles the `fact` tactic', async () => {
61
+ const formula = "succeeds member(?x, ?l1) => succeeds append(?l1, ?l2, ?l3)";
62
+ const result = await applyTactic(formula, "[fact]");
63
+ expect(result.success).toBe(true);
64
+ });
65
+
66
+ it('handles the `tot` tactic', async () => {
67
+ const formula = "terminates append(?l1, ?l2, ?l3)";
68
+ const result = await applyTactic(formula, "[tot]");
69
+ expect(result.success).toBe(true);
70
+ });
71
+
72
+ it('handles the `unfold` tactic', async () => {
73
+ const formula = "succeeds append([?h|?t], ?l2, [?h|?t3])";
74
+ const result = await applyTactic(formula, "[unfold]");
75
+ expect(result.success).toBe(true);
76
+ });
77
+
78
+ it('handles the `ex` tactic', async () => {
79
+ const formula = "ex [y]: succeeds member(?y, ?l)";
80
+ const result = await applyTactic(formula, "[ex]");
81
+ expect(result.success).toBe(true);
82
+ });
83
+
84
+ it('handles the `comp` tactic', async () => {
85
+ const formula = "succeeds append([], ?l, ?l)";
86
+ const result = await applyTactic(formula, "completion");
87
+ expect(result.success).toBe(true);
88
+ });
89
+
90
+ it('handles the `indqf` tactic', async () => {
91
+ const formula = "p(?x) => q(?x)";
92
+ const result = await applyTactic(formula, "[indqf]");
93
+ expect(result.success).toBe(true);
94
+ expect(result.derivation).toContain('assume(');
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,87 @@
1
+ import * as path from 'path';
2
+ import type { PrologBackend, ResourceLimits } from './types';
3
+
4
+ /**
5
+ * GNU Prolog backend.
6
+ *
7
+ * CLI pattern (from Makefile line 839):
8
+ * cat ./src/allg.pl | ./bin/lptp-gnu-prolog
9
+ *
10
+ * Key differences from SWI-Prolog:
11
+ * - Uses a pre-compiled binary (bin/lptp-gnu-prolog) with LPTP baked in
12
+ * - No consult needed — LPTP is compiled into the binary
13
+ * - Input is piped via stdin: echo "goal." | bin/lptp-gnu-prolog
14
+ * - No Prolog-level resource limits — Node.js timeout only
15
+ * - PREVENT_PRE_COMPILATION=true avoids .gr compilation on startup
16
+ */
17
+ export class GprologBackend implements PrologBackend {
18
+ readonly id = 'gprolog' as const;
19
+ readonly displayName = 'GNU Prolog';
20
+
21
+ private getBinaryPath(): string {
22
+ const root = process.env['LPTP_ROOT_DIR'];
23
+ if (root) return path.join(root, 'bin', 'lptp-gnu-prolog');
24
+
25
+ // Walk up from __dirname to find bin/lptp-gnu-prolog
26
+ let dir = __dirname;
27
+ for (let i = 0; i < 8; i++) {
28
+ const candidate = path.join(dir, 'bin', 'lptp-gnu-prolog');
29
+ if (require('fs').existsSync(candidate)) return candidate;
30
+ dir = path.dirname(dir);
31
+ }
32
+
33
+ throw new Error(
34
+ 'GNU Prolog binary (bin/lptp-gnu-prolog) not found. ' +
35
+ 'Set LPTP_ROOT_DIR to your LPTP checkout root, or build the binary with: cd src && make gprolog'
36
+ );
37
+ }
38
+
39
+ isAvailable(): boolean {
40
+ try {
41
+ this.getBinaryPath();
42
+ return require('fs').existsSync(this.getBinaryPath());
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * GNU Prolog has no Prolog-level resource limits.
50
+ * Returns the goal unchanged, wrapped in once/1 for determinism.
51
+ */
52
+ wrapWithLimits(goal: string, _limits?: ResourceLimits): string {
53
+ return `once(${goal})`;
54
+ }
55
+
56
+ buildExecCommand(_lptpSrc: string, tmpFile: string, _limits?: ResourceLimits): string {
57
+ const binary = this.getBinaryPath();
58
+ // LPTP is compiled into the binary — no consult needed
59
+ return `echo "exec('${tmpFile}')." | ${binary}`;
60
+ }
61
+
62
+ buildCheckCommand(_lptpSrc: string, checkPath: string, _limits?: ResourceLimits): string {
63
+ const binary = this.getBinaryPath();
64
+ return `echo "check('${checkPath}')." | ${binary}`;
65
+ }
66
+
67
+ buildCompileGrCommand(_lptpSrc: string, tmpFile: string, _limits?: ResourceLimits): string {
68
+ const binary = this.getBinaryPath();
69
+ return `echo "exec('${tmpFile}')." | ${binary}`;
70
+ }
71
+
72
+ suppressWarningsGoal(): string {
73
+ return '';
74
+ }
75
+
76
+ nodeTimeout(limits: ResourceLimits = {}, defaultMs: number = 30_000): number {
77
+ // Node.js timeout is the only guard for GNU Prolog.
78
+ if (limits.timeLimitS != null) {
79
+ return limits.timeLimitS * 1000 + 5000;
80
+ }
81
+ return defaultMs;
82
+ }
83
+
84
+ execEnv(): Record<string, string> {
85
+ return { PREVENT_PRE_COMPILATION: 'true' };
86
+ }
87
+ }
@@ -0,0 +1,70 @@
1
+ import type { BackendId, PrologBackend } from './types';
2
+ import { SwiplBackend } from './swipl';
3
+ import { ScryerBackend } from './scryer';
4
+ import { GprologBackend } from './gprolog';
5
+
6
+ let cachedBackend: PrologBackend | null = null;
7
+
8
+ function instantiate(id: BackendId): PrologBackend {
9
+ switch (id) {
10
+ case 'swipl': return new SwiplBackend();
11
+ case 'scryer': return new ScryerBackend();
12
+ case 'gprolog': return new GprologBackend();
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Returns the active Prolog backend.
18
+ *
19
+ * Selection logic:
20
+ * 1. If LPTP_PROLOG_BACKEND env var is set, use that backend (throw if unavailable).
21
+ * 2. Otherwise auto-detect in order: swipl -> scryer -> gprolog (first available wins).
22
+ * 3. If none found, throw a descriptive error.
23
+ *
24
+ * Result is cached for the lifetime of the server process.
25
+ */
26
+ export function getBackend(): PrologBackend {
27
+ if (cachedBackend) return cachedBackend;
28
+
29
+ const envChoice = process.env['LPTP_PROLOG_BACKEND'] as BackendId | undefined;
30
+
31
+ if (envChoice) {
32
+ const validIds: BackendId[] = ['swipl', 'scryer', 'gprolog'];
33
+ if (!validIds.includes(envChoice)) {
34
+ throw new Error(
35
+ `LPTP_PROLOG_BACKEND="${envChoice}" is not valid. ` +
36
+ `Choose one of: ${validIds.join(', ')}`
37
+ );
38
+ }
39
+ const backend = instantiate(envChoice);
40
+ if (!backend.isAvailable()) {
41
+ throw new Error(
42
+ `LPTP_PROLOG_BACKEND="${envChoice}" requested but ` +
43
+ `${backend.displayName} is not available on this system.`
44
+ );
45
+ }
46
+ cachedBackend = backend;
47
+ return cachedBackend;
48
+ }
49
+
50
+ // Auto-detect: try each backend in preference order
51
+ const probeOrder: BackendId[] = ['swipl', 'scryer', 'gprolog'];
52
+ for (const id of probeOrder) {
53
+ const backend = instantiate(id);
54
+ if (backend.isAvailable()) {
55
+ cachedBackend = backend;
56
+ return cachedBackend;
57
+ }
58
+ }
59
+
60
+ throw new Error(
61
+ 'No Prolog backend found. Install SWI-Prolog (swipl), ' +
62
+ 'Scryer Prolog (scryer-prolog), or build GNU Prolog binary ' +
63
+ '(cd src && make gprolog). Or set LPTP_PROLOG_BACKEND explicitly.'
64
+ );
65
+ }
66
+
67
+ /** Reset cached backend — useful for tests. */
68
+ export function resetBackendCache(): void {
69
+ cachedBackend = null;
70
+ }
@@ -0,0 +1,83 @@
1
+ import { execSync } from 'child_process';
2
+ import type { PrologBackend, ResourceLimits } from './types';
3
+
4
+ const DEFAULT_INFERENCE_LIMIT = 1_000_000;
5
+
6
+ /**
7
+ * Scryer Prolog backend.
8
+ *
9
+ * CLI pattern (from Makefile line 819):
10
+ * scryer-prolog -g "consult('./src/lptp.pl'),exec('file')." -g "halt."
11
+ *
12
+ * Key differences from SWI-Prolog:
13
+ * - Goals passed to -g need a trailing period
14
+ * - Supports call_with_inference_limit/3 from iso_ext module
15
+ * - No call_with_time_limit or call_with_depth_limit — Node.js timeout covers those
16
+ * - Quiet mode via QUIET_MODE=true environment variable
17
+ * - No message_hook suppression needed
18
+ */
19
+ export class ScryerBackend implements PrologBackend {
20
+ readonly id = 'scryer' as const;
21
+ readonly displayName = 'Scryer Prolog';
22
+
23
+ isAvailable(): boolean {
24
+ try { execSync('which scryer-prolog', { stdio: 'ignore' }); return true; }
25
+ catch { return false; }
26
+ }
27
+
28
+ /**
29
+ * Wraps a Prolog goal with Scryer-specific resource limits.
30
+ * Only inference limit is supported at the Prolog level via
31
+ * call_with_inference_limit/3 (iso_ext module).
32
+ * Time and depth limits fall back to Node.js timeout.
33
+ */
34
+ wrapWithLimits(goal: string, limits: ResourceLimits = {}): string {
35
+ let wrapped = `once(${goal})`;
36
+
37
+ const infLim = limits.inferenceLimit ?? DEFAULT_INFERENCE_LIMIT;
38
+ wrapped = `call_with_inference_limit((${wrapped}), ${infLim}, _InfRes)`;
39
+
40
+ return wrapped;
41
+ }
42
+
43
+ buildExecCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string {
44
+ const wrappedGoal = this.wrapWithLimits(
45
+ `(consult('${lptpSrc}'), exec('${tmpFile}'))`,
46
+ limits
47
+ );
48
+ return `scryer-prolog -g "${wrappedGoal}." -g "halt."`;
49
+ }
50
+
51
+ buildCheckCommand(lptpSrc: string, checkPath: string, limits?: ResourceLimits): string {
52
+ const wrappedGoal = this.wrapWithLimits(
53
+ `(consult('${lptpSrc}'), check('${checkPath}'))`,
54
+ limits ?? { inferenceLimit: 10_000_000 }
55
+ );
56
+ return `scryer-prolog -g "${wrappedGoal}." -g "halt."`;
57
+ }
58
+
59
+ buildCompileGrCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string {
60
+ const wrappedGoal = this.wrapWithLimits(
61
+ `(consult('${lptpSrc}'), exec('${tmpFile}'))`,
62
+ limits
63
+ );
64
+ return `scryer-prolog -g "${wrappedGoal}." -g "halt."`;
65
+ }
66
+
67
+ suppressWarningsGoal(): string {
68
+ return '';
69
+ }
70
+
71
+ nodeTimeout(limits: ResourceLimits = {}, defaultMs: number = 30_000): number {
72
+ // Scryer has no Prolog-level time limit, so Node.js timeout is the primary guard.
73
+ // Use the timeLimitS hint if provided, otherwise fall back to defaultMs.
74
+ if (limits.timeLimitS != null) {
75
+ return limits.timeLimitS * 1000 + 5000;
76
+ }
77
+ return defaultMs;
78
+ }
79
+
80
+ execEnv(): Record<string, string> {
81
+ return { QUIET_MODE: 'true' };
82
+ }
83
+ }
@@ -0,0 +1,88 @@
1
+ import { execSync } from 'child_process';
2
+ import type { PrologBackend, ResourceLimits } from './types';
3
+
4
+ const DEFAULT_PROLOG_TIME_LIMIT_S = 25;
5
+ const DEFAULT_INFERENCE_LIMIT = 1_000_000;
6
+
7
+ export class SwiplBackend implements PrologBackend {
8
+ readonly id = 'swipl' as const;
9
+ readonly displayName = 'SWI-Prolog';
10
+
11
+ isAvailable(): boolean {
12
+ try { execSync('which swipl', { stdio: 'ignore' }); return true; }
13
+ catch { return false; }
14
+ }
15
+
16
+ /**
17
+ * Wraps a SWI-Prolog goal with resource limits:
18
+ * once/1 — deterministic, no backtracking
19
+ * call_with_time_limit/2 — wall-clock timeout (throws time_limit_exceeded)
20
+ * call_with_inference_limit/3 — inference bound (binds Result)
21
+ * call_with_depth_limit/3 — depth bound (binds Result)
22
+ *
23
+ * If any limit is exceeded the process writes a diagnostic and halts with
24
+ * a non-zero exit code so the Node.js side captures it as an error.
25
+ */
26
+ wrapWithLimits(goal: string, limits: ResourceLimits = {}): string {
27
+ const timeS = limits.timeLimitS ?? DEFAULT_PROLOG_TIME_LIMIT_S;
28
+ const infLim = limits.inferenceLimit ?? DEFAULT_INFERENCE_LIMIT;
29
+
30
+ // Build from inside out:
31
+ // innermost: once(Goal)
32
+ // then: call_with_inference_limit(…, InfLim, InfRes)
33
+ // then: call_with_time_limit(TimeS, …)
34
+ // outermost: catch(…, time_limit_exceeded, diagnostic)
35
+ let wrapped = `once(${goal})`;
36
+
37
+ // Inference limit guard
38
+ wrapped = `call_with_inference_limit((${wrapped}), ${infLim}, _InfRes)`;
39
+
40
+ // Optional depth limit
41
+ if (limits.depthLimit != null) {
42
+ wrapped = `call_with_depth_limit((${wrapped}), ${limits.depthLimit}, _DepthRes)`;
43
+ }
44
+
45
+ // Time limit guard (outermost catch)
46
+ wrapped = `catch(call_with_time_limit(${timeS}, (${wrapped})), time_limit_exceeded, (write('TIME_LIMIT_EXCEEDED'), nl, halt(2)))`;
47
+
48
+ return wrapped;
49
+ }
50
+
51
+ buildExecCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string {
52
+ const wrappedGoal = this.wrapWithLimits(
53
+ `(consult('${lptpSrc}'), exec('${tmpFile}'))`,
54
+ limits
55
+ );
56
+ return `swipl -q -g "${wrappedGoal}, halt" -t "halt(1)"`;
57
+ }
58
+
59
+ buildCheckCommand(lptpSrc: string, checkPath: string, limits?: ResourceLimits): string {
60
+ const suppressWarnings = this.suppressWarningsGoal();
61
+ const wrappedGoal = this.wrapWithLimits(
62
+ `(${suppressWarnings}, consult('${lptpSrc}'), check('${checkPath}'))`,
63
+ limits ?? { timeLimitS: 110, inferenceLimit: 10_000_000 }
64
+ );
65
+ return `swipl -q -g "${wrappedGoal}, halt" -t "halt(1)"`;
66
+ }
67
+
68
+ buildCompileGrCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string {
69
+ const wrappedGoal = this.wrapWithLimits(
70
+ `(consult('${lptpSrc}'), exec('${tmpFile}'))`,
71
+ limits
72
+ );
73
+ return `swipl -q -g "${wrappedGoal}, halt" -t "halt(1)"`;
74
+ }
75
+
76
+ suppressWarningsGoal(): string {
77
+ return `asserta((user:message_hook(_,warning,_) :- !))`;
78
+ }
79
+
80
+ nodeTimeout(limits: ResourceLimits = {}, defaultMs: number = 30_000): number {
81
+ const timeS = limits.timeLimitS ?? DEFAULT_PROLOG_TIME_LIMIT_S;
82
+ return timeS * 1000 + 5000;
83
+ }
84
+
85
+ execEnv(): Record<string, string> {
86
+ return {};
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ export type BackendId = 'swipl' | 'scryer' | 'gprolog';
2
+
3
+ export interface ResourceLimits {
4
+ timeLimitS?: number;
5
+ inferenceLimit?: number;
6
+ depthLimit?: number;
7
+ }
8
+
9
+ export interface PrologBackend {
10
+ readonly id: BackendId;
11
+ readonly displayName: string;
12
+
13
+ /** Returns true when the backend's binary / compiled artifact is on PATH or at expected location. */
14
+ isAvailable(): boolean;
15
+
16
+ /** Build shell command that runs `exec(TmpFile)` through LPTP. */
17
+ buildExecCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string;
18
+
19
+ /** Build shell command that runs `check(CheckPath)` through LPTP. */
20
+ buildCheckCommand(lptpSrc: string, checkPath: string, limits?: ResourceLimits): string;
21
+
22
+ /** Build shell command that compiles a .gr file through LPTP. */
23
+ buildCompileGrCommand(lptpSrc: string, tmpFile: string, limits?: ResourceLimits): string;
24
+
25
+ /** Wrap a Prolog goal with backend-specific resource limits. */
26
+ wrapWithLimits(goal: string, limits?: ResourceLimits): string;
27
+
28
+ /** Return a Prolog goal that suppresses backend-specific warnings, or '' if N/A. */
29
+ suppressWarningsGoal(): string;
30
+
31
+ /** Compute Node.js exec timeout (ms) from limits. */
32
+ nodeTimeout(limits?: ResourceLimits, defaultMs?: number): number;
33
+
34
+ /** Extra environment variables for child_process.exec. */
35
+ execEnv(): Record<string, string>;
36
+ }