@gpsglobal-ai/gpsglobal 1.4.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/CHANGELOG.md +47 -0
- package/README.md +33 -0
- package/dist/cli/browser-login.d.ts +7 -0
- package/dist/cli/browser-login.js +95 -0
- package/dist/cli/doctor.d.ts +10 -0
- package/dist/cli/doctor.js +90 -0
- package/dist/cli/setup.d.ts +9 -0
- package/dist/cli/setup.js +52 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +161 -0
- package/dist/clients/gps-api.d.ts +43 -0
- package/dist/clients/gps-api.js +61 -0
- package/dist/config/env.d.ts +16 -0
- package/dist/config/env.js +78 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +20 -0
- package/dist/lib/cursor-config.d.ts +2 -0
- package/dist/lib/cursor-config.js +2 -0
- package/dist/lib/host-config.d.ts +51 -0
- package/dist/lib/host-config.js +200 -0
- package/dist/lib/jwt.d.ts +9 -0
- package/dist/lib/jwt.js +12 -0
- package/dist/lib/pkce.d.ts +6 -0
- package/dist/lib/pkce.js +11 -0
- package/dist/lib/token-policy.d.ts +14 -0
- package/dist/lib/token-policy.js +27 -0
- package/dist/server/factory.d.ts +3 -0
- package/dist/server/factory.js +9 -0
- package/dist/tools/register.d.ts +3 -0
- package/dist/tools/register.js +108 -0
- package/package.json +57 -0
- package/templates/claude-desktop-config.json +9 -0
- package/templates/cursor-mcp-oauth.json +7 -0
- package/templates/cursor-mcp.json +9 -0
- package/templates/github-mcp-oauth.json +8 -0
- package/templates/github-mcp.json +10 -0
- package/templates/vscode-mcp-oauth.json +8 -0
- package/templates/vscode-mcp.json +10 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog — gpsglobal
|
|
2
|
+
|
|
3
|
+
## 1.4.1 — 2026-06-17
|
|
4
|
+
|
|
5
|
+
- **npm scope:** publish as `@gpsglobal-ai/gpsglobal` under org `gpsglobal-ai`
|
|
6
|
+
- MCP server key remains **`gpsglobal`**; CLI bin **`gpsglobal`**
|
|
7
|
+
- Trusted Publishing workflow: `.github/workflows/npm-publish-mcp.yml`
|
|
8
|
+
- RFC 8707 `resource` parameter validation on OAuth authorize
|
|
9
|
+
- AS metadata `scopes_supported: ["mcp:read"]`
|
|
10
|
+
- `setup` writes VS Code **workspace** `.vscode/mcp.json` + fixes oauth mode
|
|
11
|
+
- E2E **MCP-25** + docs [`017`](./017-multi-host-oauth-connect.md), [`018`](./018-npm-publish-and-cicd.md)
|
|
12
|
+
|
|
13
|
+
## 1.4.0 — 2026-06-17
|
|
14
|
+
|
|
15
|
+
- **A+ security:** MCP OAuth tokens with `aud` + `scope: mcp:read` + 1h TTL (`JwtTokenProvider.createMcpOAuthToken`)
|
|
16
|
+
- **`token-policy.ts`** — HTTP transport validates audience and scope before backend proxy
|
|
17
|
+
- **G-11 closed:** `LpOwnershipGuard` no longer bypasses for `gp` role
|
|
18
|
+
- OAuth `POST /token` accepts `application/x-www-form-urlencoded` (Cursor/Claude clients)
|
|
19
|
+
- `setup --mode=oauth --host=all` writes GitHub Copilot `.github/mcp.json`
|
|
20
|
+
- E2E **MCP-22..24** — OAuth Connect chain, form token, GP API 403
|
|
21
|
+
|
|
22
|
+
## 1.3.0 — 2026-06-17
|
|
23
|
+
|
|
24
|
+
- **Renamed** npm package and MCP server identity to **`gpsglobal`** (was `@gpsglobal/mcp-server` / `gps-lp-wiki`)
|
|
25
|
+
- **`host-config.ts`** — DRY builders for Cursor, VS Code Copilot, GitHub Copilot, Claude Desktop
|
|
26
|
+
- **`setup`** default `--host=all` — writes Cursor + VS Code + Claude Desktop configs in one pass
|
|
27
|
+
- New templates: `vscode-mcp.json`, `github-mcp.json`, `github-mcp-oauth.json`
|
|
28
|
+
- E2E MCP-21 + SOTA assessment doc (`docs/57-mcp/016-sota-assessment.md`)
|
|
29
|
+
|
|
30
|
+
## 1.2.0 — 2026-06-17
|
|
31
|
+
|
|
32
|
+
- `doctor` command, `setup --mode=oauth`, OAuth redirect policy for Cursor/Claude
|
|
33
|
+
- E2E MCP-12..20 + auto-generated `evaluation-report.md`
|
|
34
|
+
|
|
35
|
+
## 1.1.0 — 2026-06-17
|
|
36
|
+
|
|
37
|
+
- `setup` — one-command browser login + Cursor `mcp.json` merge
|
|
38
|
+
- `login --browser` — PKCE loopback OAuth via backend `/api/v2/oauth/mcp-cli`
|
|
39
|
+
- `--http` — Streamable HTTP transport on `/mcp` with PRM + 401 WWW-Authenticate
|
|
40
|
+
- Docker image + `docker-compose` `gps-mcp` service on port 3100
|
|
41
|
+
- ECS `gps-mcp` service + ALB `/mcp/*` routing (infra)
|
|
42
|
+
|
|
43
|
+
## 1.0.0 — 2026-06-17
|
|
44
|
+
|
|
45
|
+
- Initial release: `list_funds`, `get_fund_wiki` tools
|
|
46
|
+
- CLI: `login`, `login --refresh`, `status`, `print-config`, `--stdio`
|
|
47
|
+
- JWT auth via `POST /api/v2/auth/signin` → `~/.gps/mcp.env`
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @gpsglobal-ai/gpsglobal
|
|
2
|
+
|
|
3
|
+
GPS LP fund wiki **MCP server** — `list_funds` and `get_fund_wiki` for Cursor, Copilot, and Claude.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|--|--|
|
|
7
|
+
| **npm** | `@gpsglobal-ai/gpsglobal` |
|
|
8
|
+
| **MCP server key** | `gpsglobal` |
|
|
9
|
+
| **CLI** | `gpsglobal` (after install) |
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @gpsglobal-ai/gpsglobal setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Browser sign-in → `~/.gps/mcp.env` → merges configs for **Cursor**, **VS Code**, and **Claude Desktop**.
|
|
18
|
+
|
|
19
|
+
| Variant | Command |
|
|
20
|
+
|---------|---------|
|
|
21
|
+
| OAuth Connect (URL-only) | `npx @gpsglobal-ai/gpsglobal setup --mode=oauth` |
|
|
22
|
+
| Health check | `npx @gpsglobal-ai/gpsglobal doctor` |
|
|
23
|
+
| Config snippets | `npx @gpsglobal-ai/gpsglobal print-config` |
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
|
|
27
|
+
- Repo spec: [`docs/57-mcp/`](../docs/57-mcp/000-INDEX.md)
|
|
28
|
+
- npm publish & CI/CD: [`docs/57-mcp/018-npm-publish-and-cicd.md`](../docs/57-mcp/018-npm-publish-and-cicd.md)
|
|
29
|
+
- Host configuration: [`docs/57-mcp/017-multi-host-oauth-connect.md`](../docs/57-mcp/017-multi-host-oauth-connect.md)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
UNLICENSED — private GPS software.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { createServer as createNetServer } from 'node:net';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { generatePkce, randomState } from '../lib/pkce.js';
|
|
7
|
+
import { normalizeApiBase, writeGpsEnvFile, DEFAULT_ENV_PATH } from '../config/env.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
async function findFreePort() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const srv = createNetServer();
|
|
12
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
13
|
+
const addr = srv.address();
|
|
14
|
+
if (!addr || typeof addr === 'string') {
|
|
15
|
+
reject(new Error('Could not bind loopback port'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const port = addr.port;
|
|
19
|
+
srv.close(() => resolve(port));
|
|
20
|
+
});
|
|
21
|
+
srv.on('error', reject);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function openBrowser(url) {
|
|
25
|
+
const cmd = process.platform === 'darwin' ? `open "${url}"` :
|
|
26
|
+
process.platform === 'win32' ? `start "" "${url}"` :
|
|
27
|
+
`xdg-open "${url}"`;
|
|
28
|
+
exec(cmd, () => undefined);
|
|
29
|
+
}
|
|
30
|
+
export async function browserLogin(apiBase) {
|
|
31
|
+
const base = normalizeApiBase(apiBase);
|
|
32
|
+
const port = await findFreePort();
|
|
33
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
34
|
+
const { codeVerifier, codeChallenge } = generatePkce();
|
|
35
|
+
const state = randomState();
|
|
36
|
+
const authorizeUrl = new URL(`${base}/api/v2/oauth/mcp-cli/authorize`);
|
|
37
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
38
|
+
authorizeUrl.searchParams.set('state', state);
|
|
39
|
+
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
40
|
+
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
41
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
42
|
+
const server = http.createServer((req, res) => {
|
|
43
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
44
|
+
if (url.pathname !== '/callback') {
|
|
45
|
+
res.writeHead(404).end('Not found');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const code = url.searchParams.get('code');
|
|
49
|
+
const returnedState = url.searchParams.get('state');
|
|
50
|
+
if (!code || returnedState !== state) {
|
|
51
|
+
res.writeHead(400).end('Invalid callback');
|
|
52
|
+
reject(new Error('OAuth callback missing code or state mismatch'));
|
|
53
|
+
server.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
57
|
+
res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;background:#09090b;color:#fafafa;display:flex;align-items:center;justify-content:center;min-height:100vh">
|
|
58
|
+
<div style="text-align:center"><h1 style="color:#d4af37">GPS MCP connected</h1><p>You can close this tab and return to your terminal.</p></div></body></html>`);
|
|
59
|
+
resolve(code);
|
|
60
|
+
server.close();
|
|
61
|
+
});
|
|
62
|
+
server.listen(port, '127.0.0.1');
|
|
63
|
+
server.on('error', reject);
|
|
64
|
+
});
|
|
65
|
+
console.log(`Opening browser for GPS sign-in…\n${authorizeUrl.href}\n`);
|
|
66
|
+
openBrowser(authorizeUrl.href);
|
|
67
|
+
const code = await Promise.race([
|
|
68
|
+
codePromise,
|
|
69
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser login timed out after 3 minutes')), 180_000)),
|
|
70
|
+
]);
|
|
71
|
+
const tokenResp = await axios.post(`${base}/api/v2/oauth/mcp-cli/token`, {
|
|
72
|
+
grant_type: 'authorization_code',
|
|
73
|
+
code,
|
|
74
|
+
redirect_uri: redirectUri,
|
|
75
|
+
code_verifier: codeVerifier,
|
|
76
|
+
}, { validateStatus: () => true });
|
|
77
|
+
if (tokenResp.status >= 400) {
|
|
78
|
+
const desc = tokenResp.data?.error_description ?? tokenResp.statusText;
|
|
79
|
+
throw new Error(`Token exchange failed: ${desc}`);
|
|
80
|
+
}
|
|
81
|
+
const data = tokenResp.data;
|
|
82
|
+
writeGpsEnvFile({
|
|
83
|
+
apiBase: base,
|
|
84
|
+
accessToken: data.access_token,
|
|
85
|
+
lpId: data.lp_id,
|
|
86
|
+
role: data.role,
|
|
87
|
+
username: data.username,
|
|
88
|
+
}, DEFAULT_ENV_PATH);
|
|
89
|
+
return {
|
|
90
|
+
accessToken: data.access_token,
|
|
91
|
+
lpId: data.lp_id,
|
|
92
|
+
username: data.username,
|
|
93
|
+
role: data.role,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { GpsApiClient } from '../clients/gps-api.js';
|
|
3
|
+
import { loadGpsConfig, normalizeApiBase, DEFAULT_ENV_PATH } from '../config/env.js';
|
|
4
|
+
import { decodeGpsJwt } from '../lib/jwt.js';
|
|
5
|
+
export async function runDoctor() {
|
|
6
|
+
const checks = [];
|
|
7
|
+
let config;
|
|
8
|
+
try {
|
|
9
|
+
config = loadGpsConfig();
|
|
10
|
+
checks.push({ name: 'credentials', ok: true, detail: `~/.gps/mcp.env · lp=${config.lpId}` });
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
checks.push({ name: 'credentials', ok: false, detail: msg });
|
|
15
|
+
return { ok: false, checks };
|
|
16
|
+
}
|
|
17
|
+
const claims = decodeGpsJwt(config.accessToken);
|
|
18
|
+
if (!claims.lpId) {
|
|
19
|
+
checks.push({ name: 'jwt', ok: false, detail: 'Token missing lpId claim' });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
checks.push({ name: 'jwt', ok: true, detail: `role=${claims.role ?? config.role}` });
|
|
23
|
+
}
|
|
24
|
+
const apiBase = normalizeApiBase(config.apiBase);
|
|
25
|
+
try {
|
|
26
|
+
const health = await axios.get(`${apiBase}/api/health`, { timeout: 10_000, validateStatus: () => true });
|
|
27
|
+
checks.push({
|
|
28
|
+
name: 'backend',
|
|
29
|
+
ok: health.status === 200,
|
|
30
|
+
detail: `${apiBase}/api/health → ${health.status}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
checks.push({
|
|
35
|
+
name: 'backend',
|
|
36
|
+
ok: false,
|
|
37
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const client = new GpsApiClient(config);
|
|
42
|
+
const funds = await client.listFunds();
|
|
43
|
+
checks.push({
|
|
44
|
+
name: 'vault',
|
|
45
|
+
ok: funds.length >= 0,
|
|
46
|
+
detail: `${funds.length} fund(s) visible`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
checks.push({
|
|
51
|
+
name: 'vault',
|
|
52
|
+
ok: false,
|
|
53
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const mcpUrl = process.env.GPS_MCP_PUBLIC_URL ?? 'http://localhost:3100';
|
|
57
|
+
try {
|
|
58
|
+
const mcpHealth = await axios.get(`${mcpUrl}/health`, { timeout: 5_000, validateStatus: () => true });
|
|
59
|
+
if (mcpHealth.status === 404) {
|
|
60
|
+
checks.push({ name: 'mcp-http', ok: true, detail: 'not running (stdio mode OK)' });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
checks.push({
|
|
64
|
+
name: 'mcp-http',
|
|
65
|
+
ok: mcpHealth.status === 200,
|
|
66
|
+
detail: `${mcpUrl}/health → ${mcpHealth.status}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
checks.push({ name: 'mcp-http', ok: true, detail: 'not running (stdio mode OK)' });
|
|
72
|
+
}
|
|
73
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
74
|
+
}
|
|
75
|
+
export function printDoctor(result) {
|
|
76
|
+
console.log('GPS MCP doctor\n');
|
|
77
|
+
for (const c of result.checks) {
|
|
78
|
+
const icon = c.ok ? '✓' : '✗';
|
|
79
|
+
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
80
|
+
}
|
|
81
|
+
console.log();
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
console.log('Fix: npx gpsglobal setup');
|
|
84
|
+
console.log(` or: npx gpsglobal login --refresh`);
|
|
85
|
+
console.log(` env: ${DEFAULT_ENV_PATH}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log('All checks passed. Restart Cursor if tools are not visible.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SetupMode = 'stdio' | 'remote' | 'oauth' | 'both';
|
|
2
|
+
export type SetupHost = 'cursor' | 'vscode' | 'claude-desktop' | 'claude-code' | 'all';
|
|
3
|
+
export interface SetupOptions {
|
|
4
|
+
apiBase?: string;
|
|
5
|
+
host?: SetupHost;
|
|
6
|
+
useBrowser?: boolean;
|
|
7
|
+
mode?: SetupMode;
|
|
8
|
+
}
|
|
9
|
+
export declare function runSetup(options: SetupOptions): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { browserLogin } from './browser-login.js';
|
|
2
|
+
import { DEFAULT_ENV_PATH, normalizeApiBase } from '../config/env.js';
|
|
3
|
+
import { MCP_SERVER_KEY, PACKAGE, claudeCodeAddCommand, mergeVsCodeConfig, mergeVsCodeWorkspaceConfig, mergeClaudeDesktopConfig, mergeCursorConfig, buildOAuthConnectEntry, buildRemoteEntry, buildStdioEntry, resolveMcpPublicUrl, writeAllHostConfigs, } from '../lib/host-config.js';
|
|
4
|
+
export async function runSetup(options) {
|
|
5
|
+
const apiBase = normalizeApiBase(options.apiBase ?? process.env.GPS_API_BASE ?? 'http://localhost:8080');
|
|
6
|
+
const host = options.host ?? 'all';
|
|
7
|
+
const useBrowser = options.useBrowser ?? true;
|
|
8
|
+
const mode = options.mode ?? 'stdio';
|
|
9
|
+
const mcpPublic = resolveMcpPublicUrl(apiBase);
|
|
10
|
+
console.log(`GPS MCP setup — configure ${MCP_SERVER_KEY} for your AI tools\n`);
|
|
11
|
+
if (useBrowser) {
|
|
12
|
+
const result = await browserLogin(apiBase);
|
|
13
|
+
console.log(`\n✓ Signed in as ${result.username} (${result.lpId})`);
|
|
14
|
+
console.log(`✓ Credentials saved to ${DEFAULT_ENV_PATH}`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log('Using existing ~/.gps/mcp.env');
|
|
18
|
+
}
|
|
19
|
+
if (host === 'all') {
|
|
20
|
+
const results = writeAllHostConfigs(DEFAULT_ENV_PATH, mode === 'both' ? 'stdio' : mode, mcpPublic);
|
|
21
|
+
for (const r of results) {
|
|
22
|
+
const icon = r.written ? '✓' : '○';
|
|
23
|
+
console.log(`${icon} ${r.host}: ${r.path}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`\n Claude Code (CLI): ${claudeCodeAddCommand(mcpPublic, mode === 'oauth' ? 'oauth' : 'remote')}`);
|
|
26
|
+
console.log(' Then run `/mcp` in Claude Code → Connect if prompted.');
|
|
27
|
+
}
|
|
28
|
+
else if (host === 'cursor') {
|
|
29
|
+
const entry = mode === 'oauth' ? buildOAuthConnectEntry(mcpPublic)
|
|
30
|
+
: mode === 'remote' ? buildRemoteEntry(mcpPublic)
|
|
31
|
+
: buildStdioEntry(DEFAULT_ENV_PATH);
|
|
32
|
+
console.log(`✓ Cursor → ${mergeCursorConfig(entry)}`);
|
|
33
|
+
}
|
|
34
|
+
else if (host === 'vscode') {
|
|
35
|
+
const entry = mode === 'oauth' ? buildOAuthConnectEntry(mcpPublic)
|
|
36
|
+
: mode === 'remote' ? buildRemoteEntry(mcpPublic)
|
|
37
|
+
: buildStdioEntry(DEFAULT_ENV_PATH);
|
|
38
|
+
const transport = mode === 'stdio' ? 'stdio' : 'http';
|
|
39
|
+
console.log(`✓ VS Code Copilot (user) → ${mergeVsCodeConfig(entry, transport)}`);
|
|
40
|
+
console.log(`✓ VS Code Copilot (workspace) → ${mergeVsCodeWorkspaceConfig(entry, transport)}`);
|
|
41
|
+
console.log(' Restart VS Code or run: MCP: List Servers');
|
|
42
|
+
}
|
|
43
|
+
else if (host === 'claude-desktop') {
|
|
44
|
+
console.log(`✓ Claude Desktop → ${mergeClaudeDesktopConfig(buildStdioEntry(DEFAULT_ENV_PATH))}`);
|
|
45
|
+
console.log(' Restart Claude Desktop.');
|
|
46
|
+
}
|
|
47
|
+
else if (host === 'claude-code') {
|
|
48
|
+
console.log(`\nRun:\n ${claudeCodeAddCommand(mcpPublic, mode === 'oauth' ? 'oauth' : 'remote')}`);
|
|
49
|
+
}
|
|
50
|
+
console.log(`\nVerify: npx ${PACKAGE} doctor`);
|
|
51
|
+
console.log(`Done. Ask your AI: "list my GPS funds using ${MCP_SERVER_KEY}"`);
|
|
52
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { GpsApiClient } from './clients/gps-api.js';
|
|
5
|
+
import { DEFAULT_ENV_PATH, loadGpsConfig, normalizeApiBase, writeGpsEnvFile, } from './config/env.js';
|
|
6
|
+
import { startStdioServer, startHttpFromEnv } from './index.js';
|
|
7
|
+
import { browserLogin } from './cli/browser-login.js';
|
|
8
|
+
import { runSetup } from './cli/setup.js';
|
|
9
|
+
import { runDoctor, printDoctor } from './cli/doctor.js';
|
|
10
|
+
import { MCP_SERVER_KEY, PACKAGE, buildMcpServersConfig, buildOAuthConnectEntry, buildRemoteEntry, buildStdioEntry, buildVsCodeServersConfig, resolveMcpPublicUrl, } from './lib/host-config.js';
|
|
11
|
+
function printConfig(envPath) {
|
|
12
|
+
const apiBase = normalizeApiBase(process.env.GPS_API_BASE ?? 'http://localhost:8080');
|
|
13
|
+
const mcpPublic = resolveMcpPublicUrl(apiBase);
|
|
14
|
+
const stdio = buildStdioEntry(envPath);
|
|
15
|
+
const remote = buildRemoteEntry(mcpPublic);
|
|
16
|
+
const oauth = buildOAuthConnectEntry(mcpPublic);
|
|
17
|
+
console.log(`
|
|
18
|
+
═══ Easiest: one command (all major hosts) ═══
|
|
19
|
+
npx ${PACKAGE} setup
|
|
20
|
+
npx ${PACKAGE} setup --mode=oauth ← OAuth Connect (URL-only)
|
|
21
|
+
|
|
22
|
+
═══ Cursor / Claude Desktop (mcpServers) ═══
|
|
23
|
+
${JSON.stringify(buildMcpServersConfig(stdio), null, 2)}
|
|
24
|
+
|
|
25
|
+
═══ VS Code / GitHub Copilot (servers) ═══
|
|
26
|
+
${JSON.stringify(buildVsCodeServersConfig(stdio), null, 2)}
|
|
27
|
+
|
|
28
|
+
═══ Remote HTTP ═══
|
|
29
|
+
${JSON.stringify(buildMcpServersConfig(remote), null, 2)}
|
|
30
|
+
|
|
31
|
+
═══ OAuth Connect (no token in JSON) ═══
|
|
32
|
+
${JSON.stringify(buildMcpServersConfig(oauth), null, 2)}
|
|
33
|
+
|
|
34
|
+
Server key: ${MCP_SERVER_KEY}
|
|
35
|
+
Docs: docs/57-mcp/010-client-configuration.md
|
|
36
|
+
API base: ${apiBase}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
async function promptHidden(question) {
|
|
40
|
+
const rl = readline.createInterface({ input, output });
|
|
41
|
+
try {
|
|
42
|
+
return await rl.question(question);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runLogin(refresh, useBrowser) {
|
|
49
|
+
const apiBase = normalizeApiBase(process.env.GPS_API_BASE ?? 'http://localhost:8080');
|
|
50
|
+
if (useBrowser) {
|
|
51
|
+
const result = await browserLogin(apiBase);
|
|
52
|
+
console.log(`\n✓ Credentials saved to ${DEFAULT_ENV_PATH}`);
|
|
53
|
+
if (refresh)
|
|
54
|
+
console.log('Token refreshed.');
|
|
55
|
+
console.log(` LP ID: ${result.lpId}`);
|
|
56
|
+
console.log(` Role: ${result.role}`);
|
|
57
|
+
printConfig(DEFAULT_ENV_PATH);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const defaultUser = process.env.GPS_USERNAME ?? 'user';
|
|
61
|
+
const username = (await promptHidden(`GPS username [${defaultUser}]: `)).trim() || defaultUser;
|
|
62
|
+
const password = await promptHidden('GPS password: ');
|
|
63
|
+
if (!password)
|
|
64
|
+
throw new Error('Password is required');
|
|
65
|
+
const body = await GpsApiClient.signIn(apiBase, username, password);
|
|
66
|
+
writeGpsEnvFile({
|
|
67
|
+
apiBase,
|
|
68
|
+
accessToken: body.accessToken,
|
|
69
|
+
lpId: body.lpId,
|
|
70
|
+
role: body.role,
|
|
71
|
+
username: body.username,
|
|
72
|
+
});
|
|
73
|
+
console.log(`\n✓ Credentials saved to ${DEFAULT_ENV_PATH} (mode 600)`);
|
|
74
|
+
if (refresh)
|
|
75
|
+
console.log('Token refreshed.');
|
|
76
|
+
console.log(` LP ID: ${body.lpId}`);
|
|
77
|
+
console.log(` Role: ${body.role}`);
|
|
78
|
+
printConfig(DEFAULT_ENV_PATH);
|
|
79
|
+
}
|
|
80
|
+
function runStatus() {
|
|
81
|
+
try {
|
|
82
|
+
const cfg = loadGpsConfig();
|
|
83
|
+
const masked = cfg.accessToken.length > 12
|
|
84
|
+
? `${cfg.accessToken.slice(0, 6)}…${cfg.accessToken.slice(-4)}`
|
|
85
|
+
: '(set)';
|
|
86
|
+
console.log(`GPS_API_BASE=${cfg.apiBase}`);
|
|
87
|
+
console.log(`GPS_LP_ID=${cfg.lpId}`);
|
|
88
|
+
console.log(`GPS_ROLE=${cfg.role}`);
|
|
89
|
+
console.log(`GPS_ACCESS_TOKEN=${masked}`);
|
|
90
|
+
console.log(`Env file: ${process.env.GPS_MCP_ENV_PATH ?? DEFAULT_ENV_PATH}`);
|
|
91
|
+
console.log(`\nIf tools fail with 401, run: npx ${PACKAGE} login --refresh`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
console.error(msg);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function main() {
|
|
100
|
+
const args = process.argv.slice(2);
|
|
101
|
+
if (args.includes('--stdio')) {
|
|
102
|
+
await startStdioServer();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (args.includes('--http')) {
|
|
106
|
+
await startHttpFromEnv();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const cmd = args[0];
|
|
110
|
+
if (!cmd || cmd === 'help' || args.includes('--help')) {
|
|
111
|
+
console.log(`Usage:
|
|
112
|
+
npx ${PACKAGE} setup ← easiest: all hosts (Cursor, VS Code, Claude Desktop)
|
|
113
|
+
npx ${PACKAGE} setup --mode=oauth ← Cursor Connect (URL-only, no envFile)
|
|
114
|
+
npx ${PACKAGE} doctor ← verify credentials + backend
|
|
115
|
+
npx ${PACKAGE} login [--browser] [--refresh]
|
|
116
|
+
npx ${PACKAGE} status
|
|
117
|
+
npx ${PACKAGE} print-config
|
|
118
|
+
npx ${PACKAGE} --stdio
|
|
119
|
+
npx ${PACKAGE} --http
|
|
120
|
+
`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (cmd === 'setup') {
|
|
124
|
+
const hostArg = args.find((a) => a.startsWith('--host='));
|
|
125
|
+
const modeArg = args.find((a) => a.startsWith('--mode='));
|
|
126
|
+
const host = hostArg?.split('=')[1];
|
|
127
|
+
const mode = modeArg?.split('=')[1];
|
|
128
|
+
await runSetup({
|
|
129
|
+
host: host ?? 'all',
|
|
130
|
+
useBrowser: !args.includes('--no-browser'),
|
|
131
|
+
mode: mode ?? 'stdio',
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (cmd === 'doctor') {
|
|
136
|
+
const result = await runDoctor();
|
|
137
|
+
printDoctor(result);
|
|
138
|
+
if (!result.ok)
|
|
139
|
+
process.exit(1);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (cmd === 'login') {
|
|
143
|
+
await runLogin(args.includes('--refresh'), args.includes('--browser'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (cmd === 'status') {
|
|
147
|
+
runStatus();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (cmd === 'print-config') {
|
|
151
|
+
printConfig(process.env.GPS_MCP_ENV_PATH ?? DEFAULT_ENV_PATH);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.error(`Unknown command: ${cmd}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
main().catch((err) => {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
console.error(msg);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type GpsEnvConfig } from '../config/env.js';
|
|
2
|
+
export interface ApiResponse<T> {
|
|
3
|
+
success: boolean;
|
|
4
|
+
body?: T;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SignInBody {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
lpId: string;
|
|
10
|
+
username: string;
|
|
11
|
+
role: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FundListItem {
|
|
14
|
+
fundId: string;
|
|
15
|
+
metadata?: {
|
|
16
|
+
fund_name?: string;
|
|
17
|
+
fund_subtitle?: string;
|
|
18
|
+
closing_date?: string;
|
|
19
|
+
};
|
|
20
|
+
hasExtraction?: boolean;
|
|
21
|
+
hasWiki?: boolean;
|
|
22
|
+
strategy?: string;
|
|
23
|
+
geography?: string;
|
|
24
|
+
sector?: string;
|
|
25
|
+
fundSummary?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface WikiContentBody {
|
|
28
|
+
lp_id: string;
|
|
29
|
+
fund_id: string;
|
|
30
|
+
fund_name: string;
|
|
31
|
+
has_wiki: boolean;
|
|
32
|
+
content: string;
|
|
33
|
+
wiki_status: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class GpsApiClient {
|
|
36
|
+
private readonly http;
|
|
37
|
+
readonly config: GpsEnvConfig;
|
|
38
|
+
constructor(config: GpsEnvConfig);
|
|
39
|
+
static signIn(apiBase: string, username: string, password: string): Promise<SignInBody>;
|
|
40
|
+
private request;
|
|
41
|
+
listFunds(lpId?: string): Promise<FundListItem[]>;
|
|
42
|
+
getWikiContent(lpId: string, fundId: string): Promise<WikiContentBody>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { normalizeApiBase } from '../config/env.js';
|
|
3
|
+
import { decodeGpsJwt } from '../lib/jwt.js';
|
|
4
|
+
function unwrap(json) {
|
|
5
|
+
if (json && typeof json === 'object' && 'success' in json) {
|
|
6
|
+
if (!json.success) {
|
|
7
|
+
throw new Error(json.message ?? 'GPS API request failed');
|
|
8
|
+
}
|
|
9
|
+
if (json.body !== undefined)
|
|
10
|
+
return json.body;
|
|
11
|
+
}
|
|
12
|
+
return json;
|
|
13
|
+
}
|
|
14
|
+
export class GpsApiClient {
|
|
15
|
+
http;
|
|
16
|
+
config;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
const claims = decodeGpsJwt(config.accessToken);
|
|
19
|
+
this.config = {
|
|
20
|
+
...config,
|
|
21
|
+
lpId: config.lpId || claims.lpId || '',
|
|
22
|
+
role: config.role || claims.role || 'lp',
|
|
23
|
+
username: config.username || claims.sub || '',
|
|
24
|
+
};
|
|
25
|
+
this.http = axios.create({
|
|
26
|
+
baseURL: `${normalizeApiBase(config.apiBase)}/api/v2`,
|
|
27
|
+
headers: { Authorization: `Bearer ${config.accessToken}` },
|
|
28
|
+
timeout: 60_000,
|
|
29
|
+
validateStatus: () => true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
static async signIn(apiBase, username, password) {
|
|
33
|
+
const resp = await axios.post(`${normalizeApiBase(apiBase)}/api/v2/auth/signin`, { username, password }, { validateStatus: () => true });
|
|
34
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
35
|
+
throw new Error('Sign-in failed: invalid username or password');
|
|
36
|
+
}
|
|
37
|
+
const json = resp.data;
|
|
38
|
+
const body = unwrap(json);
|
|
39
|
+
if (!body.accessToken || !body.lpId) {
|
|
40
|
+
throw new Error('Sign-in response missing accessToken or lpId');
|
|
41
|
+
}
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
async request(method, url) {
|
|
45
|
+
const resp = await this.http.request({ method, url });
|
|
46
|
+
if (resp.status === 401) {
|
|
47
|
+
throw new Error('Authentication expired or invalid. Run: npx @gpsglobal-ai/gpsglobal login --refresh');
|
|
48
|
+
}
|
|
49
|
+
if (resp.status === 403) {
|
|
50
|
+
throw new Error('Access denied. Your account may not have LP vault access.');
|
|
51
|
+
}
|
|
52
|
+
return unwrap(resp.data);
|
|
53
|
+
}
|
|
54
|
+
async listFunds(lpId) {
|
|
55
|
+
const id = lpId ?? this.config.lpId;
|
|
56
|
+
return this.request('get', `/triage/${encodeURIComponent(id)}`);
|
|
57
|
+
}
|
|
58
|
+
async getWikiContent(lpId, fundId) {
|
|
59
|
+
return this.request('get', `/triage/${encodeURIComponent(lpId)}/${encodeURIComponent(fundId)}/wiki-content`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const DEFAULT_ENV_PATH: string;
|
|
2
|
+
export interface GpsEnvConfig {
|
|
3
|
+
apiBase: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
lpId: string;
|
|
6
|
+
role: string;
|
|
7
|
+
username: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
10
|
+
export declare function loadGpsConfig(envPath?: string): GpsEnvConfig;
|
|
11
|
+
export declare function normalizeApiBase(base: string): string;
|
|
12
|
+
export declare function writeGpsEnvFile(config: Omit<GpsEnvConfig, 'role' | 'username'> & {
|
|
13
|
+
role: string;
|
|
14
|
+
username: string;
|
|
15
|
+
}, envPath?: string): void;
|
|
16
|
+
export declare function assertMcpEligibleRole(role: string): void;
|