context-vault 2.0.1
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/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/cli.js +588 -0
- package/package.json +30 -0
- package/smithery.yaml +10 -0
- package/src/capture/README.md +23 -0
- package/src/capture/file-ops.js +75 -0
- package/src/capture/formatters.js +29 -0
- package/src/capture/index.js +91 -0
- package/src/core/README.md +20 -0
- package/src/core/categories.js +50 -0
- package/src/core/config.js +76 -0
- package/src/core/files.js +114 -0
- package/src/core/frontmatter.js +108 -0
- package/src/core/status.js +105 -0
- package/src/index/README.md +28 -0
- package/src/index/db.js +138 -0
- package/src/index/embed.js +56 -0
- package/src/index/index.js +258 -0
- package/src/retrieve/README.md +19 -0
- package/src/retrieve/index.js +173 -0
- package/src/server/README.md +44 -0
- package/src/server/helpers.js +29 -0
- package/src/server/index.js +82 -0
- package/src/server/tools.js +211 -0
- package/ui/Context.applescript +36 -0
- package/ui/index.html +1377 -0
- package/ui/serve.js +473 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Server (Coordinator)
|
|
2
|
+
|
|
3
|
+
The MCP server entry point. This is the only place where layers cross. It wires together Capture, Index, and Retrieve into MCP tool handlers.
|
|
4
|
+
|
|
5
|
+
## Coordinator Pattern
|
|
6
|
+
|
|
7
|
+
Each tool handler orchestrates layers sequentially:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
save_context → writeEntry(ctx, data) [Capture Layer]
|
|
11
|
+
→ indexEntry(ctx, entry) [Index Layer]
|
|
12
|
+
|
|
13
|
+
get_context → hybridSearch(ctx, query) [Retrieve Layer]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
No layer calls another layer directly — the server coordinates all cross-layer operations.
|
|
17
|
+
|
|
18
|
+
## Auto-Reindex
|
|
19
|
+
|
|
20
|
+
On the first tool call per session, the server runs a full reindex to ensure the search index matches the files on disk. This is transparent to the agent — no manual reindex tool needed.
|
|
21
|
+
|
|
22
|
+
## Context Object (`ctx`)
|
|
23
|
+
|
|
24
|
+
Bundles shared state passed to all layers:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
{ db, config, stmts, embed, insertVec, deleteVec }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## MCP Tools
|
|
31
|
+
|
|
32
|
+
| Tool | Layers Used | Description |
|
|
33
|
+
|------|-------------|-------------|
|
|
34
|
+
| `get_context` | Retrieve | Hybrid FTS5 + vector search |
|
|
35
|
+
| `save_context` | Capture → Index | Write-through knowledge capture |
|
|
36
|
+
| `context_status` | Core (status) | Diagnostics |
|
|
37
|
+
|
|
38
|
+
## Dependency Rule
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
server/ → core/, capture/, index/, retrieve/ (all layers)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is the only module allowed to import across layer boundaries.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* helpers.js — Shared MCP response helpers and validation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── MCP Response Helpers ────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export function ok(text) {
|
|
8
|
+
return { content: [{ type: "text", text }] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function err(text, code = "UNKNOWN") {
|
|
12
|
+
return { content: [{ type: "text", text }], isError: true, code };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Validation Helpers ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function ensureVaultExists(config) {
|
|
18
|
+
if (!config.vaultDirExists) {
|
|
19
|
+
return err(`Vault directory not found: ${config.vaultDir}. Run context_status for diagnostics.`, "VAULT_NOT_FOUND");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureValidKind(kind) {
|
|
25
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(kind)) {
|
|
26
|
+
return err("Required: kind (lowercase alphanumeric, e.g. 'insight', 'reference')", "INVALID_KIND");
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
11
|
+
|
|
12
|
+
import { resolveConfig } from "../core/config.js";
|
|
13
|
+
import { embed } from "../index/embed.js";
|
|
14
|
+
import { initDatabase, prepareStatements, insertVec, deleteVec } from "../index/db.js";
|
|
15
|
+
import { registerTools } from "./tools.js";
|
|
16
|
+
|
|
17
|
+
// ─── Config Resolution ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const config = resolveConfig();
|
|
20
|
+
|
|
21
|
+
// Create directories
|
|
22
|
+
mkdirSync(config.dataDir, { recursive: true });
|
|
23
|
+
mkdirSync(config.vaultDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Write .context-mcp marker (always update to reflect current version)
|
|
26
|
+
const markerPath = join(config.vaultDir, ".context-mcp");
|
|
27
|
+
const markerData = existsSync(markerPath) ? JSON.parse(readFileSync(markerPath, "utf-8")) : {};
|
|
28
|
+
writeFileSync(markerPath, JSON.stringify({ created: markerData.created || new Date().toISOString(), version: pkg.version }, null, 2) + "\n");
|
|
29
|
+
|
|
30
|
+
// Update existence flag after directory creation
|
|
31
|
+
config.vaultDirExists = existsSync(config.vaultDir);
|
|
32
|
+
|
|
33
|
+
// Startup diagnostics
|
|
34
|
+
console.error(`[context-mcp] Vault: ${config.vaultDir}`);
|
|
35
|
+
console.error(`[context-mcp] Database: ${config.dbPath}`);
|
|
36
|
+
console.error(`[context-mcp] Dev dir: ${config.devDir}`);
|
|
37
|
+
if (!config.vaultDirExists) {
|
|
38
|
+
console.error(`[context-mcp] WARNING: Vault directory not found!`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Database Init ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
let db, stmts;
|
|
44
|
+
try {
|
|
45
|
+
db = initDatabase(config.dbPath);
|
|
46
|
+
stmts = prepareStatements(db);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(`[context-mcp] Database init failed: ${e.message}`);
|
|
49
|
+
console.error(`[context-mcp] DB path: ${config.dbPath}`);
|
|
50
|
+
console.error(`[context-mcp] Try deleting the DB file and restarting: rm "${config.dbPath}"`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ctx = {
|
|
55
|
+
db,
|
|
56
|
+
config,
|
|
57
|
+
stmts,
|
|
58
|
+
embed,
|
|
59
|
+
insertVec: (rowid, embedding) => insertVec(stmts, rowid, embedding),
|
|
60
|
+
deleteVec: (rowid) => deleteVec(stmts, rowid),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ─── MCP Server ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const server = new McpServer(
|
|
66
|
+
{ name: "context-mcp", version: pkg.version },
|
|
67
|
+
{ capabilities: { tools: {} } }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
registerTools(server, ctx);
|
|
71
|
+
|
|
72
|
+
// ─── Graceful Shutdown ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function shutdown() {
|
|
75
|
+
try { db.close(); } catch {}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
process.on("SIGINT", shutdown);
|
|
79
|
+
process.on("SIGTERM", shutdown);
|
|
80
|
+
|
|
81
|
+
const transport = new StdioServerTransport();
|
|
82
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.js — MCP tool registrations
|
|
3
|
+
*
|
|
4
|
+
* Three tools: save_context (write), get_context (read), context_status (diag).
|
|
5
|
+
* Auto-reindex runs transparently on first tool call per session.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
import { captureAndIndex } from "../capture/index.js";
|
|
12
|
+
import { hybridSearch } from "../retrieve/index.js";
|
|
13
|
+
import { reindex, indexEntry } from "../index/index.js";
|
|
14
|
+
import { gatherVaultStatus } from "../core/status.js";
|
|
15
|
+
import { categoryFor } from "../core/categories.js";
|
|
16
|
+
import { normalizeKind } from "../core/files.js";
|
|
17
|
+
import { ok, err, ensureVaultExists, ensureValidKind } from "./helpers.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register all MCP tools on the server.
|
|
21
|
+
*
|
|
22
|
+
* @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
|
|
23
|
+
* @param {{ db, config, stmts, embed, insertVec, deleteVec }} ctx
|
|
24
|
+
*/
|
|
25
|
+
export function registerTools(server, ctx) {
|
|
26
|
+
const { config } = ctx;
|
|
27
|
+
|
|
28
|
+
// ─── Auto-Reindex (runs once per session, on first tool call) ──────────────
|
|
29
|
+
|
|
30
|
+
let reindexDone = false;
|
|
31
|
+
let reindexPromise = null;
|
|
32
|
+
let reindexAttempts = 0;
|
|
33
|
+
let reindexFailed = false;
|
|
34
|
+
const MAX_REINDEX_ATTEMPTS = 2;
|
|
35
|
+
|
|
36
|
+
async function ensureIndexed() {
|
|
37
|
+
if (reindexDone) return;
|
|
38
|
+
if (reindexPromise) return reindexPromise;
|
|
39
|
+
reindexPromise = reindex(ctx, { fullSync: true })
|
|
40
|
+
.then((stats) => {
|
|
41
|
+
reindexDone = true;
|
|
42
|
+
const total = stats.added + stats.updated + stats.removed;
|
|
43
|
+
if (total > 0) {
|
|
44
|
+
console.error(`[context-mcp] Auto-reindex: +${stats.added} ~${stats.updated} -${stats.removed} (${stats.unchanged} unchanged)`);
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch((e) => {
|
|
48
|
+
reindexAttempts++;
|
|
49
|
+
console.error(`[context-mcp] Auto-reindex failed (attempt ${reindexAttempts}/${MAX_REINDEX_ATTEMPTS}): ${e.message}`);
|
|
50
|
+
if (reindexAttempts >= MAX_REINDEX_ATTEMPTS) {
|
|
51
|
+
console.error(`[context-mcp] Giving up on auto-reindex. Run \`context-mcp reindex\` manually to diagnose.`);
|
|
52
|
+
reindexDone = true;
|
|
53
|
+
reindexFailed = true;
|
|
54
|
+
} else {
|
|
55
|
+
reindexPromise = null; // Allow retry on next tool call
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return reindexPromise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── get_context (read) ────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
server.tool(
|
|
64
|
+
"get_context",
|
|
65
|
+
"Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context.",
|
|
66
|
+
{
|
|
67
|
+
query: z.string().describe("Search query (natural language or keywords)"),
|
|
68
|
+
kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
69
|
+
category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
|
|
70
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (entries must match at least one)"),
|
|
71
|
+
since: z.string().optional().describe("ISO date, return entries created after this"),
|
|
72
|
+
until: z.string().optional().describe("ISO date, return entries created before this"),
|
|
73
|
+
limit: z.number().optional().describe("Max results to return (default 10)"),
|
|
74
|
+
},
|
|
75
|
+
async ({ query, kind, category, tags, since, until, limit }) => {
|
|
76
|
+
if (!query?.trim()) return err("Required: query (non-empty string)", "INVALID_INPUT");
|
|
77
|
+
await ensureIndexed();
|
|
78
|
+
|
|
79
|
+
const kindFilter = kind ? normalizeKind(kind) : null;
|
|
80
|
+
const sorted = await hybridSearch(ctx, query, {
|
|
81
|
+
kindFilter,
|
|
82
|
+
categoryFilter: category || null,
|
|
83
|
+
since: since || null,
|
|
84
|
+
until: until || null,
|
|
85
|
+
limit: limit || 10,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Post-filter by tags if provided
|
|
89
|
+
const filtered = tags?.length
|
|
90
|
+
? sorted.filter((r) => {
|
|
91
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
92
|
+
return tags.some((t) => entryTags.includes(t));
|
|
93
|
+
})
|
|
94
|
+
: sorted;
|
|
95
|
+
|
|
96
|
+
if (!filtered.length) return ok("No results found for: " + query);
|
|
97
|
+
|
|
98
|
+
const lines = [];
|
|
99
|
+
if (reindexFailed) lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-mcp reindex\` to fix.\n`);
|
|
100
|
+
lines.push(`## Results for "${query}" (${filtered.length} matches)\n`);
|
|
101
|
+
for (const r of filtered) {
|
|
102
|
+
const meta = r.meta ? JSON.parse(r.meta) : {};
|
|
103
|
+
lines.push(`### ${r.title || "(untitled)"} [${r.kind}/${r.category}]`);
|
|
104
|
+
lines.push(`Score: ${r.score.toFixed(3)} | Tags: ${r.tags || "none"} | File: ${r.file_path || "n/a"}`);
|
|
105
|
+
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
return ok(lines.join("\n"));
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ─── save_context (write) ──────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
server.tool(
|
|
115
|
+
"save_context",
|
|
116
|
+
"Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind.",
|
|
117
|
+
{
|
|
118
|
+
kind: z.string().describe("Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind)"),
|
|
119
|
+
title: z.string().optional().describe("Entry title (optional for insights)"),
|
|
120
|
+
body: z.string().describe("Main content"),
|
|
121
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization and search"),
|
|
122
|
+
meta: z.any().optional().describe("Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"),
|
|
123
|
+
folder: z.string().optional().describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
|
|
124
|
+
source: z.string().optional().describe("Where this knowledge came from"),
|
|
125
|
+
identity_key: z.string().optional().describe("Required for entity kinds (contact, project, tool, source). The unique identifier for this entity."),
|
|
126
|
+
expires_at: z.string().optional().describe("ISO date for TTL expiry"),
|
|
127
|
+
},
|
|
128
|
+
async ({ kind, title, body, tags, meta, folder, source, identity_key, expires_at }) => {
|
|
129
|
+
const vaultErr = ensureVaultExists(config);
|
|
130
|
+
if (vaultErr) return vaultErr;
|
|
131
|
+
const kindErr = ensureValidKind(kind);
|
|
132
|
+
if (kindErr) return kindErr;
|
|
133
|
+
if (!body?.trim()) return err("Required: body (non-empty string)", "INVALID_INPUT");
|
|
134
|
+
|
|
135
|
+
// Validate: entity kinds require identity_key
|
|
136
|
+
if (categoryFor(kind) === "entity" && !identity_key) {
|
|
137
|
+
return err(`Entity kind "${kind}" requires identity_key`, "MISSING_IDENTITY_KEY");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await ensureIndexed();
|
|
141
|
+
|
|
142
|
+
const mergedMeta = { ...(meta || {}) };
|
|
143
|
+
if (folder) mergedMeta.folder = folder;
|
|
144
|
+
const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
|
|
145
|
+
|
|
146
|
+
const entry = await captureAndIndex(ctx, { kind, title, body, meta: finalMeta, tags, source, folder, identity_key, expires_at }, indexEntry);
|
|
147
|
+
return ok(`Saved ${kind} ${entry.id}\nFile: ${entry.filePath}${title ? "\nTitle: " + title : ""}`);
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// ─── context_status (diagnostics) ──────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
server.tool(
|
|
154
|
+
"context_status",
|
|
155
|
+
"Show vault health: resolved config, file counts per kind, database size, and any issues. Use to verify setup or troubleshoot.",
|
|
156
|
+
{},
|
|
157
|
+
() => {
|
|
158
|
+
const status = gatherVaultStatus(ctx);
|
|
159
|
+
|
|
160
|
+
const lines = [
|
|
161
|
+
`## Vault Status`,
|
|
162
|
+
``,
|
|
163
|
+
`Vault: ${config.vaultDir} (exists: ${config.vaultDirExists}, ${status.fileCount} files)`,
|
|
164
|
+
`Database: ${config.dbPath} (exists: ${existsSync(config.dbPath)}, ${status.dbSize})`,
|
|
165
|
+
`Dev dir: ${config.devDir}`,
|
|
166
|
+
`Data dir: ${config.dataDir}`,
|
|
167
|
+
`Config: ${config.configPath}`,
|
|
168
|
+
`Resolved via: ${status.resolvedFrom}`,
|
|
169
|
+
`Schema: v5 (categories)`,
|
|
170
|
+
``,
|
|
171
|
+
`### Indexed`,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if (status.kindCounts.length) {
|
|
175
|
+
for (const { kind, c } of status.kindCounts) lines.push(`- ${c} ${kind}s`);
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(`- (empty)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (status.categoryCounts.length) {
|
|
181
|
+
lines.push(``);
|
|
182
|
+
lines.push(`### Categories`);
|
|
183
|
+
for (const { category, c } of status.categoryCounts) lines.push(`- ${category}: ${c}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (status.subdirs.length) {
|
|
187
|
+
lines.push(``);
|
|
188
|
+
lines.push(`### Disk Directories`);
|
|
189
|
+
for (const { name, count } of status.subdirs) lines.push(`- ${name}/: ${count} files`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (status.stalePaths) {
|
|
193
|
+
lines.push(``);
|
|
194
|
+
lines.push(`### Stale Paths Detected`);
|
|
195
|
+
lines.push(`DB contains ${status.staleCount} paths not matching current vault dir.`);
|
|
196
|
+
lines.push(`Auto-reindex will fix this on next search or save.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (status.embeddingStatus) {
|
|
200
|
+
const { indexed, total, missing } = status.embeddingStatus;
|
|
201
|
+
if (missing > 0) {
|
|
202
|
+
lines.push(``);
|
|
203
|
+
lines.push(`### Embeddings`);
|
|
204
|
+
lines.push(`${indexed}/${total} entries have embeddings (${missing} missing)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return ok(lines.join("\n"));
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Context MCP UI Launcher
|
|
2
|
+
-- Starts the server if not running, then opens the dashboard
|
|
3
|
+
|
|
4
|
+
on run
|
|
5
|
+
-- Dynamic node path
|
|
6
|
+
set nodePath to do shell script "which node"
|
|
7
|
+
|
|
8
|
+
-- Dynamic serve.js path (relative to this script's bundle)
|
|
9
|
+
set scriptDir to do shell script "dirname " & quoted form of (POSIX path of (path to me))
|
|
10
|
+
set serverScript to scriptDir & "/serve.js"
|
|
11
|
+
set serverPort to "3141"
|
|
12
|
+
set serverURL to "http://localhost:" & serverPort
|
|
13
|
+
|
|
14
|
+
-- Check if server is already running on port
|
|
15
|
+
set isRunning to false
|
|
16
|
+
try
|
|
17
|
+
do shell script "lsof -ti:" & serverPort
|
|
18
|
+
set isRunning to true
|
|
19
|
+
end try
|
|
20
|
+
|
|
21
|
+
-- Start server via nohup so it survives after this app exits
|
|
22
|
+
if not isRunning then
|
|
23
|
+
do shell script "nohup " & quoted form of nodePath & " " & quoted form of serverScript & " > /tmp/context-mcp.log 2>&1 &"
|
|
24
|
+
-- Wait for server to be ready
|
|
25
|
+
repeat 10 times
|
|
26
|
+
delay 0.5
|
|
27
|
+
try
|
|
28
|
+
do shell script "curl -s -o /dev/null -w '%{http_code}' " & serverURL & "/api/discover"
|
|
29
|
+
exit repeat
|
|
30
|
+
end try
|
|
31
|
+
end repeat
|
|
32
|
+
end if
|
|
33
|
+
|
|
34
|
+
-- Open in default browser
|
|
35
|
+
open location serverURL
|
|
36
|
+
end run
|