@bridgenet-tech/spear-data 1.5.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.
- package/README.md +112 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.js +107 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/infra-status.d.ts +2 -0
- package/dist/tools/infra-status.js +103 -0
- package/dist/tools/infra-status.js.map +1 -0
- package/dist/tools/ingest-status.d.ts +2 -0
- package/dist/tools/ingest-status.js +105 -0
- package/dist/tools/ingest-status.js.map +1 -0
- package/dist/tools/schema.d.ts +2 -0
- package/dist/tools/schema.js +147 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/semantic-search.d.ts +2 -0
- package/dist/tools/semantic-search.js +432 -0
- package/dist/tools/semantic-search.js.map +1 -0
- package/dist/tools/sql-query.d.ts +2 -0
- package/dist/tools/sql-query.js +138 -0
- package/dist/tools/sql-query.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @bridgenet-tech/spear-data
|
|
2
|
+
|
|
3
|
+
MCP server for read-only Spear PSA data exploration. Connects to PostgreSQL (Neon cloud or local Docker) with pgvector for semantic search across 150k+ tickets and 765k+ embeddings.
|
|
4
|
+
|
|
5
|
+
## First install
|
|
6
|
+
|
|
7
|
+
### 1. Set environment variables
|
|
8
|
+
|
|
9
|
+
Add these to your shell profile (`~/.bashrc` or `~/.zshrc`):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export POSTGRES_URL="postgresql://user:password@ep-xxx.us-east-2.aws.neon.tech/spear?sslmode=require"
|
|
13
|
+
export GOOGLE_API_KEY="your-google-ai-studio-api-key"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`POSTGRES_URL` is required. Get the Neon connection string from the Neon dashboard (project: spear).
|
|
17
|
+
|
|
18
|
+
`GOOGLE_API_KEY` is optional but enables semantic search. Without it, semantic search falls back to the Mac Studio embedding server (only reachable on the local network).
|
|
19
|
+
|
|
20
|
+
After editing your profile, restart your terminal or run `source ~/.bashrc`.
|
|
21
|
+
|
|
22
|
+
### 2. Configure the MCP server in Claude Code
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add spear-data -- npx @bridgenet-tech/spear-data --target cloud
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This registers the server so Claude Code starts it automatically on every session. The `--target cloud` flag tells it to connect to Neon (the cloud database).
|
|
29
|
+
|
|
30
|
+
### 3. Verify it works
|
|
31
|
+
|
|
32
|
+
Open Claude Code and ask:
|
|
33
|
+
|
|
34
|
+
> How many tickets do we have?
|
|
35
|
+
|
|
36
|
+
Claude should use the `spear_sql_query` tool and return a count from the database.
|
|
37
|
+
|
|
38
|
+
## Updating to the latest version
|
|
39
|
+
|
|
40
|
+
npx caches packages locally. To get a new version after a bug fix or feature update:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Clear the npx cache and verify the new version
|
|
44
|
+
npx @bridgenet-tech/spear-data@latest --version
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If npx still serves a stale version:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Nuclear option: clear the entire npx cache
|
|
51
|
+
rm -rf ~/.npm/_npx
|
|
52
|
+
npx @bridgenet-tech/spear-data@latest --version
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No need to re-run `claude mcp add` -- the existing registration will pick up the new version automatically when you clear the cache.
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| Tool | What it does |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `spear_sql_query` | Run read-only SQL SELECT queries against the Spear database. Only SELECT, WITH (CTE), and EXPLAIN are allowed -- all write operations are blocked. Returns results with full transparency metadata: target database, execution time, row count, and the SQL that ran. |
|
|
62
|
+
| `spear_semantic_search` | Search for similar tickets using natural language via pgvector. Embeds your query and finds tickets by cosine similarity. Great for fuzzy queries like "printer problems at law firms" or "recurring VPN issues" where exact SQL matching would miss results. |
|
|
63
|
+
| `spear_ingest_status` | Shows the state of the Autotask data ingest pipeline: sync status per entity, last sync timestamp, record counts, data freshness, and any errors. |
|
|
64
|
+
| `spear_schema` | Returns the database schema -- tables, columns, data types, nullable status, and foreign key relationships. Use this before writing SQL queries to understand the data model. |
|
|
65
|
+
| `spear_infra_status` | Infrastructure health check: database reachability, size, connection latency, pgvector extension status, and embedding counts. |
|
|
66
|
+
|
|
67
|
+
Every tool response includes a `_meta` object with source (postgres/pgvector), target (local/cloud), execution time (ms), and the query that ran.
|
|
68
|
+
|
|
69
|
+
## CLI options
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
npx @bridgenet-tech/spear-data [options]
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--target <local|cloud> Database target (default: cloud)
|
|
76
|
+
--version, -v Print version and exit
|
|
77
|
+
--help, -h Print help and exit
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Environment variables
|
|
81
|
+
|
|
82
|
+
| Variable | Required | Description |
|
|
83
|
+
|----------|----------|-------------|
|
|
84
|
+
| `POSTGRES_URL` | Yes | PostgreSQL connection string. For cloud: Neon connection string with `?sslmode=require`. For local: `postgresql://spear:spear@localhost:5432/spear`. |
|
|
85
|
+
| `GOOGLE_API_KEY` | No | Google AI Studio API key for query embedding in semantic search. Falls back to Ollama on the local network if not set. |
|
|
86
|
+
|
|
87
|
+
## For developers
|
|
88
|
+
|
|
89
|
+
If you have the repo cloned, the server also works directly with Bun:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd /path/to/spear
|
|
93
|
+
bun run ./mcp/spear-data/src/index.ts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
In repo mode, it reads `.env.local` and `.env.production` for database credentials instead of environment variables, and supports both `local` and `cloud` targets simultaneously.
|
|
97
|
+
|
|
98
|
+
### Building
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cd mcp/spear-data
|
|
102
|
+
npm install
|
|
103
|
+
npm run build # outputs to dist/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Publishing to GitHub Packages
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm login --registry=https://npm.pkg.github.com --scope=@bridgenet-tech
|
|
110
|
+
cd mcp/spear-data
|
|
111
|
+
npm publish
|
|
112
|
+
```
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
export declare function parseEnvFile(path: string): Record<string, string>;
|
|
3
|
+
export declare function setDefaultTarget(target: Target): void;
|
|
4
|
+
export declare function getDefaultTarget(): Target;
|
|
5
|
+
export declare function setStandalone(standalone: boolean): void;
|
|
6
|
+
export declare function isStandalone(): boolean;
|
|
7
|
+
export type Target = "local" | "cloud";
|
|
8
|
+
export declare function getPostgres(target: Target): postgres.Sql;
|
|
9
|
+
export declare function pingPostgres(target: Target): Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
ms: number;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Environment loading — parse .env files manually (no dotenv dependency)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export function parseEnvFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
const content = readFileSync(path, "utf-8");
|
|
10
|
+
const vars = {};
|
|
11
|
+
for (const line of content.split("\n")) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
14
|
+
continue;
|
|
15
|
+
const eqIdx = trimmed.indexOf("=");
|
|
16
|
+
if (eqIdx === -1)
|
|
17
|
+
continue;
|
|
18
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
19
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
20
|
+
// Strip surrounding quotes
|
|
21
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
22
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
23
|
+
value = value.slice(1, -1);
|
|
24
|
+
}
|
|
25
|
+
vars[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return vars;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Standalone mode detection — when running via npx outside the repo,
|
|
35
|
+
// POSTGRES_URL comes from the environment and --target is set on CLI.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Default target for all tools. Set once at startup from --target CLI arg.
|
|
39
|
+
* In standalone mode, this is the ONLY target that works.
|
|
40
|
+
* In repo mode, tools can still override per-call.
|
|
41
|
+
*/
|
|
42
|
+
let _defaultTarget = "cloud";
|
|
43
|
+
let _standalone = false;
|
|
44
|
+
export function setDefaultTarget(target) {
|
|
45
|
+
_defaultTarget = target;
|
|
46
|
+
}
|
|
47
|
+
export function getDefaultTarget() {
|
|
48
|
+
return _defaultTarget;
|
|
49
|
+
}
|
|
50
|
+
export function setStandalone(standalone) {
|
|
51
|
+
_standalone = standalone;
|
|
52
|
+
}
|
|
53
|
+
export function isStandalone() {
|
|
54
|
+
return _standalone;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Env file loading — only in repo mode (files may not exist in standalone)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Use import.meta.dirname (Node 21.2+, Bun) with fallback
|
|
60
|
+
const selfDir = import.meta.dirname ?? ".";
|
|
61
|
+
const repoRoot = resolve(selfDir, "../../../");
|
|
62
|
+
const localEnv = parseEnvFile(resolve(repoRoot, ".env.local"));
|
|
63
|
+
const prodEnv = parseEnvFile(resolve(repoRoot, ".env.production"));
|
|
64
|
+
let localSql = null;
|
|
65
|
+
let cloudSql = null;
|
|
66
|
+
export function getPostgres(target) {
|
|
67
|
+
if (target === "local") {
|
|
68
|
+
if (!localSql) {
|
|
69
|
+
// In standalone mode OR repo mode: env var first, then .env.local fallback
|
|
70
|
+
const url = process.env.POSTGRES_URL ?? localEnv.POSTGRES_URL;
|
|
71
|
+
if (!url)
|
|
72
|
+
throw new Error(_standalone
|
|
73
|
+
? "POSTGRES_URL environment variable is required. Set it to your Neon connection string."
|
|
74
|
+
: "POSTGRES_URL not found in .env.local or environment");
|
|
75
|
+
localSql = postgres(url, { max: 3 });
|
|
76
|
+
}
|
|
77
|
+
return localSql;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (!cloudSql) {
|
|
81
|
+
// In standalone mode: env var is the ONLY source
|
|
82
|
+
// In repo mode: .env.production first, then env var fallback
|
|
83
|
+
const url = _standalone
|
|
84
|
+
? process.env.POSTGRES_URL
|
|
85
|
+
: (prodEnv.POSTGRES_URL ?? process.env.POSTGRES_URL);
|
|
86
|
+
if (!url)
|
|
87
|
+
throw new Error(_standalone
|
|
88
|
+
? "POSTGRES_URL environment variable is required. Set it to your Neon connection string."
|
|
89
|
+
: "POSTGRES_URL not found in .env.production or environment");
|
|
90
|
+
cloudSql = postgres(url, { max: 3 });
|
|
91
|
+
}
|
|
92
|
+
return cloudSql;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Check if a target is reachable
|
|
96
|
+
export async function pingPostgres(target) {
|
|
97
|
+
const start = performance.now();
|
|
98
|
+
try {
|
|
99
|
+
const sql = getPostgres(target);
|
|
100
|
+
await sql `SELECT 1`;
|
|
101
|
+
return { ok: true, ms: Math.round(performance.now() - start) };
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
return { ok: false, ms: Math.round(performance.now() - start), error: e.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAE9E,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5C,MAAM,IAAI,GAA2B,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAClD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,KAAK,KAAK,CAAC,CAAC;gBAAE,SAAS;YAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5C,2BAA2B;YAC3B,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC9C,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACnD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7B,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACpB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,qEAAqE;AACrE,sEAAsE;AACtE,8EAA8E;AAE9E;;;;GAIG;AACH,IAAI,cAAc,GAAW,OAAO,CAAC;AACrC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,cAAc,GAAG,MAAM,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAAmB;IAC/C,WAAW,GAAG,UAAU,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAE9E,0DAA0D;AAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC;AAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AAC/C,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;AAC/D,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAQnE,IAAI,QAAQ,GAAwB,IAAI,CAAC;AACzC,IAAI,QAAQ,GAAwB,IAAI,CAAC;AAEzC,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,2EAA2E;YAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,CAAC;YAC9D,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CACvB,WAAW;oBACT,CAAC,CAAC,uFAAuF;oBACzF,CAAC,CAAC,qDAAqD,CAC1D,CAAC;YACF,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,iDAAiD;YACjD,6DAA6D;YAC7D,MAAM,GAAG,GAAG,WAAW;gBACrB,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY;gBAC1B,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YACvD,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CACvB,WAAW;oBACT,CAAC,CAAC,uFAAuF;oBACzF,CAAC,CAAC,0DAA0D,CAC/D,CAAC;YACF,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc;IAC/C,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,GAAG,CAAA,UAAU,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;IACjE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;IAC/F,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* spear-data MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Read-only data exploration for the Spear PSA database.
|
|
6
|
+
* Connects to both local (Docker pgvector) and cloud (Neon pgvector) databases.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @bridgenet-tech/spear-data --target cloud # standalone
|
|
10
|
+
* npx @bridgenet-tech/spear-data --env-file ~/.config/.env # with env file
|
|
11
|
+
* bun run src/index.ts # dev (in-repo)
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* spear-data MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Read-only data exploration for the Spear PSA database.
|
|
6
|
+
* Connects to both local (Docker pgvector) and cloud (Neon pgvector) databases.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @bridgenet-tech/spear-data --target cloud # standalone
|
|
10
|
+
* npx @bridgenet-tech/spear-data --env-file ~/.config/.env # with env file
|
|
11
|
+
* bun run src/index.ts # dev (in-repo)
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { registerSqlQuery } from "./tools/sql-query.js";
|
|
16
|
+
import { registerSemanticSearch } from "./tools/semantic-search.js";
|
|
17
|
+
import { registerIngestStatus } from "./tools/ingest-status.js";
|
|
18
|
+
import { registerSchema } from "./tools/schema.js";
|
|
19
|
+
import { registerInfraStatus } from "./tools/infra-status.js";
|
|
20
|
+
import { setDefaultTarget, setStandalone, parseEnvFile } from "./db.js";
|
|
21
|
+
const VERSION = "1.5.0";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// CLI argument parsing
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
let target = "cloud";
|
|
27
|
+
let envFile = null;
|
|
28
|
+
let version = false;
|
|
29
|
+
let help = false;
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === "--target" && i + 1 < argv.length) {
|
|
33
|
+
const val = argv[++i];
|
|
34
|
+
if (val === "local" || val === "cloud") {
|
|
35
|
+
target = val;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.error(`Invalid target: ${val}. Use 'local' or 'cloud'.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (arg === "--env-file") {
|
|
43
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
44
|
+
console.error("Error: --env-file option requires a value.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
envFile = argv[++i];
|
|
48
|
+
}
|
|
49
|
+
else if (arg === "--version" || arg === "-v") {
|
|
50
|
+
version = true;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--help" || arg === "-h") {
|
|
53
|
+
help = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { target, envFile, version, help };
|
|
57
|
+
}
|
|
58
|
+
const args = parseArgs(process.argv.slice(2));
|
|
59
|
+
if (args.version) {
|
|
60
|
+
console.log(`@bridgenet-tech/spear-data v${VERSION}`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
if (args.help) {
|
|
64
|
+
console.log(`@bridgenet-tech/spear-data v${VERSION}
|
|
65
|
+
|
|
66
|
+
MCP server for read-only Spear PSA data exploration.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npx @bridgenet-tech/spear-data [options]
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--target <local|cloud> Database target (default: cloud)
|
|
73
|
+
--env-file <path> Load environment variables from a file
|
|
74
|
+
--version, -v Print version and exit
|
|
75
|
+
--help, -h Print this help and exit
|
|
76
|
+
|
|
77
|
+
Environment variables:
|
|
78
|
+
POSTGRES_URL PostgreSQL connection string (required)
|
|
79
|
+
GOOGLE_API_KEY Google AI Studio API key (for semantic search)
|
|
80
|
+
|
|
81
|
+
Install as MCP server in Claude Code:
|
|
82
|
+
claude mcp add spear-data -- npx @bridgenet-tech/spear-data --target cloud
|
|
83
|
+
`);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Load env file if specified (before any other env lookups)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
if (args.envFile) {
|
|
90
|
+
const vars = parseEnvFile(args.envFile);
|
|
91
|
+
const loaded = Object.keys(vars).length;
|
|
92
|
+
if (loaded === 0) {
|
|
93
|
+
console.error(`Warning: --env-file ${args.envFile} yielded no variables (file missing or empty)`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Inject into process.env — does NOT overwrite existing env vars
|
|
97
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
98
|
+
if (!(key in process.env)) {
|
|
99
|
+
process.env[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.error(`Loaded ${loaded} env vars from ${args.envFile}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Detect standalone mode: if POSTGRES_URL is in env AND we're not in the
|
|
107
|
+
// repo (no .env.local/.env.production nearby), we're running standalone.
|
|
108
|
+
// A simpler heuristic: if invoked via npx or with --target flag, standalone.
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
const invokedViaPackageManager = process.argv[1]?.includes("node_modules") || false;
|
|
111
|
+
const hasEnvVar = !!process.env.POSTGRES_URL;
|
|
112
|
+
if (invokedViaPackageManager || hasEnvVar) {
|
|
113
|
+
setStandalone(true);
|
|
114
|
+
}
|
|
115
|
+
setDefaultTarget(args.target);
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Server setup
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
const server = new McpServer({
|
|
120
|
+
name: "spear-data",
|
|
121
|
+
version: VERSION
|
|
122
|
+
});
|
|
123
|
+
// Register all tools
|
|
124
|
+
registerSqlQuery(server);
|
|
125
|
+
registerSemanticSearch(server);
|
|
126
|
+
registerIngestStatus(server);
|
|
127
|
+
registerSchema(server);
|
|
128
|
+
registerInfraStatus(server);
|
|
129
|
+
// Start stdio transport
|
|
130
|
+
async function main() {
|
|
131
|
+
const transport = new StdioServerTransport();
|
|
132
|
+
await server.connect(transport);
|
|
133
|
+
console.error(`spear-data MCP server v${VERSION} running via stdio (target: ${args.target})`);
|
|
134
|
+
}
|
|
135
|
+
main().catch(error => {
|
|
136
|
+
console.error("Fatal:", error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPostgres, pingPostgres, isStandalone, getDefaultTarget } from "../db.js";
|
|
3
|
+
const InputSchema = z.object({}).strict();
|
|
4
|
+
export function registerInfraStatus(server) {
|
|
5
|
+
server.registerTool("spear_infra_status", {
|
|
6
|
+
title: "Infrastructure Status",
|
|
7
|
+
description: `Show infrastructure status across all data stores — both local and cloud.
|
|
8
|
+
|
|
9
|
+
Returns for each target (local, cloud):
|
|
10
|
+
- Postgres: reachable, database size, connection latency, pgvector extension status, embedding count
|
|
11
|
+
|
|
12
|
+
Use this to answer: "Are my databases up?", "How big is the database?", "How many embeddings?", "What's the latency to cloud?"
|
|
13
|
+
|
|
14
|
+
No parameters needed — checks everything automatically.`,
|
|
15
|
+
inputSchema: InputSchema,
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
destructiveHint: false,
|
|
19
|
+
idempotentHint: true,
|
|
20
|
+
openWorldHint: true
|
|
21
|
+
}
|
|
22
|
+
}, async () => {
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
// In standalone mode, only check the configured target
|
|
25
|
+
const targets = isStandalone()
|
|
26
|
+
? [getDefaultTarget()]
|
|
27
|
+
: ["local", "cloud"];
|
|
28
|
+
// Run all checks in parallel
|
|
29
|
+
const pings = await Promise.all(targets.map(t => pingPostgres(t)));
|
|
30
|
+
// Get detailed info from reachable databases
|
|
31
|
+
const details = {};
|
|
32
|
+
for (let i = 0; i < targets.length; i++) {
|
|
33
|
+
const target = targets[i];
|
|
34
|
+
const ping = pings[i];
|
|
35
|
+
if (ping.ok) {
|
|
36
|
+
try {
|
|
37
|
+
const sql = getPostgres(target);
|
|
38
|
+
const [sizeResult] = await sql `SELECT pg_size_pretty(pg_database_size(current_database())) as db_size`;
|
|
39
|
+
const [versionResult] = await sql `SELECT version() as pg_version`;
|
|
40
|
+
const [pgvectorCheck] = await sql `SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector') AS installed`;
|
|
41
|
+
// Count embeddings across all entity types
|
|
42
|
+
const entityTables = [
|
|
43
|
+
"tickets", "organizations", "contacts", "agents",
|
|
44
|
+
"assets", "contracts", "projects", "tasks", "time_entries",
|
|
45
|
+
"ticket_note_chunks", "teams_message_chunks"
|
|
46
|
+
];
|
|
47
|
+
const embeddingCounts = {};
|
|
48
|
+
let totalEmbedded = 0;
|
|
49
|
+
const counts = await Promise.all(entityTables.map(async (table) => {
|
|
50
|
+
const [result] = await sql.unsafe(`SELECT COUNT(*) AS total, COUNT(embedding) AS embedded FROM ${table}`);
|
|
51
|
+
return {
|
|
52
|
+
table,
|
|
53
|
+
total: Number(result?.total ?? 0),
|
|
54
|
+
embedded: Number(result?.embedded ?? 0),
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
for (const c of counts) {
|
|
58
|
+
embeddingCounts[c.table] = { total: c.total, embedded: c.embedded };
|
|
59
|
+
totalEmbedded += c.embedded;
|
|
60
|
+
}
|
|
61
|
+
details[`postgres_${target}`] = {
|
|
62
|
+
reachable: true,
|
|
63
|
+
latency_ms: ping.ms,
|
|
64
|
+
db_size: sizeResult?.db_size,
|
|
65
|
+
pg_version: versionResult?.pg_version?.split(" ").slice(0, 2).join(" "),
|
|
66
|
+
pgvector_installed: pgvectorCheck?.installed ?? false,
|
|
67
|
+
total_embedded: totalEmbedded,
|
|
68
|
+
entity_counts: embeddingCounts
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
details[`postgres_${target}`] = {
|
|
73
|
+
reachable: true,
|
|
74
|
+
latency_ms: ping.ms,
|
|
75
|
+
detail_error: e.message
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
details[`postgres_${target}`] = {
|
|
81
|
+
reachable: false,
|
|
82
|
+
latency_ms: ping.ms,
|
|
83
|
+
error: ping.error
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const ms = Math.round(performance.now() - start);
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
_meta: {
|
|
93
|
+
execution_ms: ms,
|
|
94
|
+
checked_at: new Date().toISOString(),
|
|
95
|
+
standalone: isStandalone()
|
|
96
|
+
},
|
|
97
|
+
infrastructure: details
|
|
98
|
+
}, null, 2)
|
|
99
|
+
}]
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=infra-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infra-status.js","sourceRoot":"","sources":["../../src/tools/infra-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPostgres } from "../db.js";
|
|
3
|
+
const InputSchema = z.object({
|
|
4
|
+
target: z.enum(["local", "cloud"])
|
|
5
|
+
.default("cloud")
|
|
6
|
+
.describe("Which database to check: 'local' (Docker) or 'cloud' (Neon)")
|
|
7
|
+
}).strict();
|
|
8
|
+
export function registerIngestStatus(server) {
|
|
9
|
+
server.registerTool("spear_ingest_status", {
|
|
10
|
+
title: "Ingest Status",
|
|
11
|
+
description: `Show the current state of the Autotask data ingest pipeline.
|
|
12
|
+
|
|
13
|
+
Returns for each entity (organizations, contacts, agents, tickets, notes, note_chunks, assets, contracts, projects, tasks, time_entries, teams_messages, teams_message_chunks, pipeline_runs, embeddings):
|
|
14
|
+
- sync status (idle, running, success, failed)
|
|
15
|
+
- last successful sync timestamp
|
|
16
|
+
- records synced count
|
|
17
|
+
- data freshness (how long since last sync)
|
|
18
|
+
- any error messages
|
|
19
|
+
|
|
20
|
+
Also returns row counts for all tables so you can see how much data is loaded.
|
|
21
|
+
|
|
22
|
+
Use this to answer: "Is my data up to date?", "When was the last sync?", "How many tickets do we have?", "Is anything still loading?"`,
|
|
23
|
+
inputSchema: InputSchema,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
idempotentHint: true,
|
|
28
|
+
openWorldHint: false
|
|
29
|
+
}
|
|
30
|
+
}, async (params) => {
|
|
31
|
+
const start = performance.now();
|
|
32
|
+
try {
|
|
33
|
+
const sql = getPostgres(params.target);
|
|
34
|
+
// Get sync state
|
|
35
|
+
const syncState = await sql `
|
|
36
|
+
SELECT entity, status, last_sync_at, started_at, completed_at,
|
|
37
|
+
records_synced, error_message,
|
|
38
|
+
CASE
|
|
39
|
+
WHEN last_sync_at IS NULL THEN 'never synced'
|
|
40
|
+
WHEN NOW() - last_sync_at < INTERVAL '1 hour' THEN 'fresh (< 1h)'
|
|
41
|
+
WHEN NOW() - last_sync_at < INTERVAL '1 day' THEN 'recent (< 1d)'
|
|
42
|
+
WHEN NOW() - last_sync_at < INTERVAL '7 days' THEN 'stale (< 1w)'
|
|
43
|
+
ELSE 'very stale (> 1w)'
|
|
44
|
+
END as freshness,
|
|
45
|
+
EXTRACT(EPOCH FROM (NOW() - last_sync_at))::int as seconds_since_sync
|
|
46
|
+
FROM sync_state
|
|
47
|
+
ORDER BY entity
|
|
48
|
+
`;
|
|
49
|
+
// Get row counts for all tables
|
|
50
|
+
const rowCounts = await sql `
|
|
51
|
+
SELECT 'tickets' as table_name, COUNT(*)::int as row_count FROM tickets
|
|
52
|
+
UNION ALL SELECT 'organizations', COUNT(*)::int FROM organizations
|
|
53
|
+
UNION ALL SELECT 'contacts', COUNT(*)::int FROM contacts
|
|
54
|
+
UNION ALL SELECT 'agents', COUNT(*)::int FROM agents
|
|
55
|
+
UNION ALL SELECT 'ticket_notes', COUNT(*)::int FROM ticket_notes
|
|
56
|
+
UNION ALL SELECT 'ticket_note_chunks', COUNT(*)::int FROM ticket_note_chunks
|
|
57
|
+
UNION ALL SELECT 'assets', COUNT(*)::int FROM assets
|
|
58
|
+
UNION ALL SELECT 'contracts', COUNT(*)::int FROM contracts
|
|
59
|
+
UNION ALL SELECT 'projects', COUNT(*)::int FROM projects
|
|
60
|
+
UNION ALL SELECT 'tasks', COUNT(*)::int FROM tasks
|
|
61
|
+
UNION ALL SELECT 'time_entries', COUNT(*)::int FROM time_entries
|
|
62
|
+
UNION ALL SELECT 'teams_messages', COUNT(*)::int FROM teams_messages
|
|
63
|
+
UNION ALL SELECT 'teams_message_chunks', COUNT(*)::int FROM teams_message_chunks
|
|
64
|
+
UNION ALL SELECT 'pipeline_runs', COUNT(*)::int FROM pipeline_runs
|
|
65
|
+
ORDER BY table_name
|
|
66
|
+
`;
|
|
67
|
+
// Get date range of ticket data
|
|
68
|
+
const dateRange = await sql `
|
|
69
|
+
SELECT MIN(created_at) as earliest_ticket,
|
|
70
|
+
MAX(created_at) as latest_ticket,
|
|
71
|
+
COUNT(*)::int as total_tickets
|
|
72
|
+
FROM tickets
|
|
73
|
+
`;
|
|
74
|
+
const ms = Math.round(performance.now() - start);
|
|
75
|
+
const result = {
|
|
76
|
+
_meta: {
|
|
77
|
+
source: "postgres",
|
|
78
|
+
target: params.target,
|
|
79
|
+
execution_ms: ms,
|
|
80
|
+
checked_at: new Date().toISOString()
|
|
81
|
+
},
|
|
82
|
+
sync_state: syncState,
|
|
83
|
+
row_counts: Object.fromEntries(rowCounts.map(r => [r.table_name, r.row_count])),
|
|
84
|
+
data_range: dateRange[0] ?? null
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
const ms = Math.round(performance.now() - start);
|
|
92
|
+
return {
|
|
93
|
+
isError: true,
|
|
94
|
+
content: [{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: JSON.stringify({
|
|
97
|
+
_meta: { source: "postgres", target: params.target, execution_ms: ms },
|
|
98
|
+
error: e.message
|
|
99
|
+
}, null, 2)
|
|
100
|
+
}]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=ingest-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-status.js","sourceRoot":"","sources":["../../src/tools/ingest-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAe,MAAM,UAAU,CAAC;AAEpD,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;SAC/B,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CAAC,6DAA6D,CAAC;CAC3E,CAAC,CAAC,MAAM,EAAE,CAAC;AAIZ,MAAM,UAAU,oBAAoB,CAAC,MAAiB;IACpD,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;QACE,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE;;;;;;;;;;;sIAWmH;QAChI,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,KAAK;SACrB;KACF,EACD,KAAK,EAAE,MAAa,EAAE,EAAE;QACtB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAEvC,iBAAiB;YACjB,MAAM,SAAS,GAAG,MAAM,GAAG,CAAA;;;;;;;;;;;;;SAa1B,CAAC;YAEF,gCAAgC;YAChC,MAAM,SAAS,GAAG,MAAM,GAAG,CAAA;;;;;;;;;;;;;;;;SAgB1B,CAAC;YAEF,gCAAgC;YAChC,MAAM,SAAS,GAAG,MAAM,GAAG,CAAA;;;;;SAK1B,CAAC;YAEF,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;YAEjD,MAAM,MAAM,GAAG;gBACb,KAAK,EAAE;oBACL,MAAM,EAAE,UAAU;oBAClB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,YAAY,EAAE,EAAE;oBAChB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACrC;gBACD,UAAU,EAAE,SAAS;gBACrB,UAAU,EAAE,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;gBAC/E,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI;aACjC,CAAC;YAEF,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;aAC5E,CAAC;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;YACjD,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;4BACnB,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE;4BACtE,KAAK,EAAG,CAAW,CAAC,OAAO;yBAC5B,EAAE,IAAI,EAAE,CAAC,CAAC;qBACZ,CAAC;aACH,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|