@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.
@@ -0,0 +1,78 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export const DEFAULT_ENV_PATH = path.join(os.homedir(), '.gps', 'mcp.env');
5
+ export function parseEnvFile(content) {
6
+ const out = {};
7
+ for (const line of content.split('\n')) {
8
+ const trimmed = line.trim();
9
+ if (!trimmed || trimmed.startsWith('#'))
10
+ continue;
11
+ const exportPrefix = trimmed.startsWith('export ') ? 7 : 0;
12
+ const body = trimmed.slice(exportPrefix);
13
+ const eq = body.indexOf('=');
14
+ if (eq <= 0)
15
+ continue;
16
+ const key = body.slice(0, eq).trim();
17
+ let value = body.slice(eq + 1).trim();
18
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
19
+ value = value.slice(1, -1);
20
+ }
21
+ out[key] = value;
22
+ }
23
+ return out;
24
+ }
25
+ export function loadGpsConfig(envPath = process.env.GPS_MCP_ENV_PATH ?? DEFAULT_ENV_PATH) {
26
+ const apiBase = process.env.GPS_API_BASE;
27
+ const accessToken = process.env.GPS_ACCESS_TOKEN;
28
+ const lpId = process.env.GPS_LP_ID;
29
+ const role = process.env.GPS_ROLE ?? 'lp';
30
+ const username = process.env.GPS_USERNAME ?? '';
31
+ if (apiBase && accessToken && lpId) {
32
+ return { apiBase: normalizeApiBase(apiBase), accessToken, lpId, role, username };
33
+ }
34
+ if (!fs.existsSync(envPath)) {
35
+ throw new Error(`GPS MCP credentials not found. Run: npx gpsglobal login\nExpected file: ${envPath}`);
36
+ }
37
+ const parsed = parseEnvFile(fs.readFileSync(envPath, 'utf8'));
38
+ const merged = {
39
+ apiBase: normalizeApiBase(parsed.GPS_API_BASE ?? 'http://localhost:8080'),
40
+ accessToken: parsed.GPS_ACCESS_TOKEN ?? '',
41
+ lpId: parsed.GPS_LP_ID ?? '',
42
+ role: parsed.GPS_ROLE ?? 'lp',
43
+ username: parsed.GPS_USERNAME ?? '',
44
+ };
45
+ if (!merged.accessToken || !merged.lpId) {
46
+ throw new Error(`Invalid ${envPath} — missing GPS_ACCESS_TOKEN or GPS_LP_ID. Run: npx gpsglobal login --refresh`);
47
+ }
48
+ return merged;
49
+ }
50
+ export function normalizeApiBase(base) {
51
+ return base.replace(/\/+$/, '');
52
+ }
53
+ export function writeGpsEnvFile(config, envPath = DEFAULT_ENV_PATH) {
54
+ const dir = path.dirname(envPath);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ const lines = [
57
+ '# GPS MCP credentials — chmod 600 — do not commit',
58
+ `export GPS_API_BASE=${config.apiBase}`,
59
+ `export GPS_ACCESS_TOKEN=${config.accessToken}`,
60
+ `export GPS_LP_ID=${config.lpId}`,
61
+ `export GPS_ROLE=${config.role}`,
62
+ `export GPS_USERNAME=${config.username}`,
63
+ '',
64
+ ];
65
+ fs.writeFileSync(envPath, lines.join('\n'), { mode: 0o600 });
66
+ try {
67
+ fs.chmodSync(envPath, 0o600);
68
+ }
69
+ catch {
70
+ // Windows may not support chmod
71
+ }
72
+ }
73
+ export function assertMcpEligibleRole(role) {
74
+ const r = role.toLowerCase();
75
+ if (r === 'gp' || r === 'content_manager') {
76
+ throw new Error('MCP fund wiki tools require an LP account. GP and content_manager roles cannot use these tools.');
77
+ }
78
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface HttpServerOptions {
2
+ port: number;
3
+ host: string;
4
+ apiBase: string;
5
+ publicBaseUrl: string;
6
+ enabled: boolean;
7
+ }
8
+ export declare function startHttpServer(opts: HttpServerOptions): Promise<void>;
package/dist/http.js ADDED
@@ -0,0 +1,115 @@
1
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { GpsApiClient } from './clients/gps-api.js';
4
+ import { assertMcpEligibleRole } from './config/env.js';
5
+ import { createGpsMcpServer } from './server/factory.js';
6
+ import { decodeGpsJwt } from './lib/jwt.js';
7
+ import { assertMcpHttpToken } from './lib/token-policy.js';
8
+ function parseBearer(req) {
9
+ const h = req.headers.authorization;
10
+ if (!h?.startsWith('Bearer '))
11
+ return null;
12
+ return h.slice(7).trim() || null;
13
+ }
14
+ function configFromToken(token, apiBase) {
15
+ const claims = decodeGpsJwt(token);
16
+ return {
17
+ apiBase,
18
+ accessToken: token,
19
+ lpId: claims.lpId ?? '',
20
+ role: claims.role ?? 'lp',
21
+ username: claims.sub ?? '',
22
+ };
23
+ }
24
+ function sendUnauthorized(res, resourceMetadataUrl, message) {
25
+ res.setHeader('WWW-Authenticate', `Bearer realm="gps-mcp", resource_metadata="${resourceMetadataUrl}"`);
26
+ res.status(401).json({ error: 'Unauthorized', message });
27
+ }
28
+ export async function startHttpServer(opts) {
29
+ const app = createMcpExpressApp({ host: opts.host });
30
+ const resourceMetadataUrl = `${opts.publicBaseUrl}/.well-known/oauth-protected-resource`;
31
+ const authServer = `${opts.apiBase}/api/v2/oauth/mcp-cli`;
32
+ app.get('/health', (_req, res) => {
33
+ if (!opts.enabled) {
34
+ res.status(503).json({ status: 'disabled', message: 'GPS MCP HTTP is disabled' });
35
+ return;
36
+ }
37
+ res.json({ status: 'ok', service: 'gpsglobal', version: '1.4.0' });
38
+ });
39
+ app.get('/.well-known/oauth-protected-resource', (_req, res) => {
40
+ res.json({
41
+ resource: `${opts.publicBaseUrl}/mcp`,
42
+ authorization_servers: [authServer],
43
+ scopes_supported: ['mcp:read'],
44
+ bearer_methods_supported: ['header'],
45
+ });
46
+ });
47
+ const handleMcpPost = async (req, res) => {
48
+ if (!opts.enabled) {
49
+ res.status(503).json({ error: 'GPS MCP is temporarily disabled' });
50
+ return;
51
+ }
52
+ const token = parseBearer(req);
53
+ if (!token) {
54
+ sendUnauthorized(res, resourceMetadataUrl, 'Bearer token required. Run: npx gpsglobal setup');
55
+ return;
56
+ }
57
+ const claims = decodeGpsJwt(token);
58
+ const config = configFromToken(token, opts.apiBase);
59
+ const strictAud = process.env.GPS_MCP_STRICT_AUD !== 'false';
60
+ try {
61
+ assertMcpHttpToken(claims, { resourceUrl: `${opts.publicBaseUrl}/mcp`, strictAudience: strictAud });
62
+ }
63
+ catch (err) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ sendUnauthorized(res, resourceMetadataUrl, msg);
66
+ return;
67
+ }
68
+ if (!config.lpId) {
69
+ sendUnauthorized(res, resourceMetadataUrl, 'Token missing lpId claim');
70
+ return;
71
+ }
72
+ try {
73
+ assertMcpEligibleRole(config.role);
74
+ const client = new GpsApiClient(config);
75
+ await client.listFunds();
76
+ const server = createGpsMcpServer(client);
77
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
78
+ await server.connect(transport);
79
+ await transport.handleRequest(req, res, req.body);
80
+ res.on('close', () => {
81
+ void transport.close();
82
+ void server.close();
83
+ });
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ if (msg.includes('401') || msg.includes('Authentication')) {
88
+ sendUnauthorized(res, resourceMetadataUrl, 'Invalid or expired token. Run: npx gpsglobal login --refresh');
89
+ return;
90
+ }
91
+ if (!res.headersSent) {
92
+ res.status(500).json({ error: 'Internal error', message: msg });
93
+ }
94
+ }
95
+ };
96
+ app.post('/mcp', async (req, res) => {
97
+ await handleMcpPost(req, res);
98
+ });
99
+ // MCP spec: unauthenticated GET /mcp → 401 + WWW-Authenticate (host OAuth discovery)
100
+ app.get('/mcp', (req, res) => {
101
+ if (!opts.enabled) {
102
+ res.status(503).json({ error: 'GPS MCP is temporarily disabled' });
103
+ return;
104
+ }
105
+ const token = parseBearer(req);
106
+ if (!token) {
107
+ sendUnauthorized(res, resourceMetadataUrl, 'Authentication required. Use Cursor Connect or run: npx gpsglobal setup');
108
+ return;
109
+ }
110
+ res.status(405).json({ error: 'Method not allowed — use POST for MCP requests' });
111
+ });
112
+ app.listen(opts.port, opts.host, () => {
113
+ console.error(`[gps-mcp] HTTP http://${opts.host}:${opts.port}/mcp (enabled=${opts.enabled})`);
114
+ });
115
+ }
@@ -0,0 +1,2 @@
1
+ export declare function startStdioServer(): Promise<void>;
2
+ export declare function startHttpFromEnv(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { GpsApiClient } from './clients/gps-api.js';
3
+ import { loadGpsConfig, normalizeApiBase } from './config/env.js';
4
+ import { createGpsMcpServer } from './server/factory.js';
5
+ import { startHttpServer } from './http.js';
6
+ export async function startStdioServer() {
7
+ const config = loadGpsConfig();
8
+ const client = new GpsApiClient(config);
9
+ const server = createGpsMcpServer(client);
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ }
13
+ export async function startHttpFromEnv() {
14
+ const port = Number(process.env.PORT ?? 3100);
15
+ const host = process.env.MCP_HOST ?? '0.0.0.0';
16
+ const apiBase = normalizeApiBase(process.env.GPS_API_BASE ?? 'http://localhost:8080');
17
+ const publicBaseUrl = normalizeApiBase(process.env.GPS_MCP_PUBLIC_URL ?? `http://localhost:${port}`);
18
+ const enabled = process.env.GPS_MCP_ENABLED !== 'false';
19
+ await startHttpServer({ port, host, apiBase, publicBaseUrl, enabled });
20
+ }
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import from host-config.js */
2
+ export { PACKAGE, MCP_SERVER_KEY, buildStdioEntry, buildRemoteEntry, buildOAuthConnectEntry, cursorConfigPath, envFilePlaceholder, mergeCursorConfig, resolveMcpPublicUrl, type StdioEntry as CursorMcpEntry, } from './host-config.js';
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import from host-config.js */
2
+ export { PACKAGE, MCP_SERVER_KEY, buildStdioEntry, buildRemoteEntry, buildOAuthConnectEntry, cursorConfigPath, envFilePlaceholder, mergeCursorConfig, resolveMcpPublicUrl, } from './host-config.js';
@@ -0,0 +1,51 @@
1
+ /** npm scoped package under org gpsglobal-ai (see docs/57-mcp/018-npm-publish-and-cicd.md). */
2
+ export declare const NPM_PACKAGE = "@gpsglobal-ai/gpsglobal";
3
+ /** MCP protocol server key in mcp.json — short, stable across hosts. */
4
+ export declare const MCP_SERVER_KEY = "gpsglobal";
5
+ /** CLI binary name after install (`npm i -g` → `gpsglobal` command). */
6
+ export declare const CLI_BIN = "gpsglobal";
7
+ /** @alias NPM_PACKAGE */
8
+ export declare const PACKAGE = "@gpsglobal-ai/gpsglobal";
9
+ export interface StdioEntry {
10
+ command: string;
11
+ args: string[];
12
+ envFile?: string;
13
+ env?: Record<string, string>;
14
+ }
15
+ export interface RemoteEntry {
16
+ url: string;
17
+ headers?: Record<string, string>;
18
+ }
19
+ export type HostId = 'cursor' | 'vscode' | 'github' | 'claude-desktop';
20
+ export interface HostConfigFile {
21
+ host: HostId;
22
+ path: string;
23
+ rootKey: 'mcpServers' | 'servers';
24
+ payload: Record<string, unknown>;
25
+ }
26
+ export declare function envFilePlaceholder(envPath: string): string;
27
+ export declare function buildStdioEntry(envPath: string): StdioEntry;
28
+ export declare function buildRemoteEntry(mcpPublicUrl: string): RemoteEntry;
29
+ export declare function buildOAuthConnectEntry(mcpPublicUrl: string): RemoteEntry;
30
+ /** Cursor / Claude Desktop — root key `mcpServers`, stdio shape. */
31
+ export declare function buildMcpServersConfig(entry: StdioEntry | RemoteEntry): Record<string, unknown>;
32
+ /** VS Code / GitHub Copilot — root key `servers`, requires `type`. */
33
+ export declare function buildVsCodeServersConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http'): Record<string, unknown>;
34
+ export declare function cursorConfigPath(): string;
35
+ export declare function vscodeUserMcpPath(): string;
36
+ /** VS Code workspace config — share with team via source control. */
37
+ export declare function vscodeWorkspaceMcpPath(projectRoot?: string): string;
38
+ export declare function claudeDesktopConfigPath(): string;
39
+ export declare function mergeCursorConfig(entry: StdioEntry | RemoteEntry, serverKey?: string): string;
40
+ export declare function mergeVsCodeConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http'): string;
41
+ export declare function mergeVsCodeWorkspaceConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http', projectRoot?: string): string;
42
+ export declare function mergeClaudeDesktopConfig(entry: StdioEntry, serverKey?: string): string;
43
+ export declare function resolveMcpPublicUrl(apiBase: string): string;
44
+ export declare function claudeCodeAddCommand(mcpPublicUrl: string, mode?: 'oauth' | 'remote'): string;
45
+ export interface SetupHostResult {
46
+ host: HostId;
47
+ path: string;
48
+ written: boolean;
49
+ }
50
+ export declare function mergeGitHubConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http', projectRoot?: string): string;
51
+ export declare function writeAllHostConfigs(envPath: string, mode: 'stdio' | 'remote' | 'oauth', mcpPublicUrl: string, projectRoot?: string): SetupHostResult[];
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Single source of truth for MCP server identity and per-host JSON configs.
3
+ * Hosts differ only by root key and field names — builders stay DRY here.
4
+ */
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ /** npm scoped package under org gpsglobal-ai (see docs/57-mcp/018-npm-publish-and-cicd.md). */
9
+ export const NPM_PACKAGE = '@gpsglobal-ai/gpsglobal';
10
+ /** MCP protocol server key in mcp.json — short, stable across hosts. */
11
+ export const MCP_SERVER_KEY = 'gpsglobal';
12
+ /** CLI binary name after install (`npm i -g` → `gpsglobal` command). */
13
+ export const CLI_BIN = 'gpsglobal';
14
+ /** @alias NPM_PACKAGE */
15
+ export const PACKAGE = NPM_PACKAGE;
16
+ export function envFilePlaceholder(envPath) {
17
+ return envPath.replace(os.homedir(), '${userHome}');
18
+ }
19
+ export function buildStdioEntry(envPath) {
20
+ return {
21
+ command: 'npx',
22
+ args: ['-y', NPM_PACKAGE, '--stdio'],
23
+ envFile: envFilePlaceholder(envPath),
24
+ };
25
+ }
26
+ export function buildRemoteEntry(mcpPublicUrl) {
27
+ return {
28
+ url: `${mcpPublicUrl.replace(/\/+$/, '')}/mcp`,
29
+ headers: {
30
+ Authorization: 'Bearer ${env:GPS_ACCESS_TOKEN}',
31
+ },
32
+ };
33
+ }
34
+ export function buildOAuthConnectEntry(mcpPublicUrl) {
35
+ return {
36
+ url: `${mcpPublicUrl.replace(/\/+$/, '')}/mcp`,
37
+ };
38
+ }
39
+ /** Cursor / Claude Desktop — root key `mcpServers`, stdio shape. */
40
+ export function buildMcpServersConfig(entry) {
41
+ return { mcpServers: { [MCP_SERVER_KEY]: entry } };
42
+ }
43
+ /** VS Code / GitHub Copilot — root key `servers`, requires `type`. */
44
+ export function buildVsCodeServersConfig(entry, transport = 'stdio') {
45
+ if ('url' in entry) {
46
+ return {
47
+ servers: {
48
+ [MCP_SERVER_KEY]: {
49
+ type: 'http',
50
+ url: entry.url,
51
+ ...(entry.headers ? { headers: entry.headers } : {}),
52
+ },
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ servers: {
58
+ [MCP_SERVER_KEY]: {
59
+ type: 'stdio',
60
+ command: entry.command,
61
+ args: entry.args,
62
+ ...(entry.envFile ? { envFile: entry.envFile } : {}),
63
+ ...(entry.env ? { env: entry.env } : {}),
64
+ },
65
+ },
66
+ };
67
+ }
68
+ export function cursorConfigPath() {
69
+ return path.join(os.homedir(), '.cursor', 'mcp.json');
70
+ }
71
+ export function vscodeUserMcpPath() {
72
+ if (process.platform === 'darwin') {
73
+ return path.join(os.homedir(), 'Library/Application Support/Code/User/mcp.json');
74
+ }
75
+ if (process.platform === 'win32') {
76
+ return path.join(process.env.APPDATA ?? os.homedir(), 'Code', 'User', 'mcp.json');
77
+ }
78
+ return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
79
+ }
80
+ /** VS Code workspace config — share with team via source control. */
81
+ export function vscodeWorkspaceMcpPath(projectRoot = process.cwd()) {
82
+ return path.join(projectRoot, '.vscode', 'mcp.json');
83
+ }
84
+ export function claudeDesktopConfigPath() {
85
+ if (process.platform === 'darwin') {
86
+ return path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
87
+ }
88
+ if (process.platform === 'win32') {
89
+ return path.join(process.env.APPDATA ?? os.homedir(), 'Claude', 'claude_desktop_config.json');
90
+ }
91
+ return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
92
+ }
93
+ function mergeJsonConfig(configPath, rootKey, serverKey, entry) {
94
+ let existing = {};
95
+ if (fs.existsSync(configPath)) {
96
+ try {
97
+ existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
98
+ }
99
+ catch {
100
+ existing = {};
101
+ }
102
+ }
103
+ const merged = {
104
+ ...existing,
105
+ [rootKey]: {
106
+ ...(existing[rootKey] ?? {}),
107
+ [serverKey]: entry,
108
+ },
109
+ };
110
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
111
+ fs.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`);
112
+ return configPath;
113
+ }
114
+ export function mergeCursorConfig(entry, serverKey = MCP_SERVER_KEY) {
115
+ return mergeJsonConfig(cursorConfigPath(), 'mcpServers', serverKey, entry);
116
+ }
117
+ function mergeVsCodeConfigAt(configPath, entry, transport = 'stdio') {
118
+ const built = buildVsCodeServersConfig(entry, transport);
119
+ const servers = built.servers[MCP_SERVER_KEY];
120
+ return mergeJsonConfig(configPath, 'servers', MCP_SERVER_KEY, servers);
121
+ }
122
+ export function mergeVsCodeConfig(entry, transport = 'stdio') {
123
+ return mergeVsCodeConfigAt(vscodeUserMcpPath(), entry, transport);
124
+ }
125
+ export function mergeVsCodeWorkspaceConfig(entry, transport = 'stdio', projectRoot = process.cwd()) {
126
+ return mergeVsCodeConfigAt(vscodeWorkspaceMcpPath(projectRoot), entry, transport);
127
+ }
128
+ export function mergeClaudeDesktopConfig(entry, serverKey = MCP_SERVER_KEY) {
129
+ return mergeJsonConfig(claudeDesktopConfigPath(), 'mcpServers', serverKey, entry);
130
+ }
131
+ export function resolveMcpPublicUrl(apiBase) {
132
+ const normalized = apiBase.replace(/\/+$/, '');
133
+ if (normalized.includes('localhost') || normalized.includes('127.0.0.1')) {
134
+ return process.env.GPS_MCP_PUBLIC_URL ?? 'http://localhost:3100';
135
+ }
136
+ return process.env.GPS_MCP_PUBLIC_URL ?? 'https://lp.gpsglobal.ai';
137
+ }
138
+ export function claudeCodeAddCommand(mcpPublicUrl, mode = 'oauth') {
139
+ const url = `${mcpPublicUrl.replace(/\/+$/, '')}/mcp`;
140
+ if (mode === 'oauth') {
141
+ return `claude mcp add --scope user --transport http ${MCP_SERVER_KEY} ${url}`;
142
+ }
143
+ return `claude mcp add --scope user --transport http ${MCP_SERVER_KEY} ${url} --header "Authorization: Bearer $GPS_ACCESS_TOKEN"`;
144
+ }
145
+ export function mergeGitHubConfig(entry, transport = 'stdio', projectRoot = process.cwd()) {
146
+ const configPath = path.join(projectRoot, '.github', 'mcp.json');
147
+ const built = buildVsCodeServersConfig(entry, transport);
148
+ const servers = built.servers[MCP_SERVER_KEY];
149
+ return mergeJsonConfig(configPath, 'servers', MCP_SERVER_KEY, servers);
150
+ }
151
+ export function writeAllHostConfigs(envPath, mode, mcpPublicUrl, projectRoot = process.cwd()) {
152
+ const results = [];
153
+ const stdio = buildStdioEntry(envPath);
154
+ const remote = buildRemoteEntry(mcpPublicUrl);
155
+ const oauth = buildOAuthConnectEntry(mcpPublicUrl);
156
+ const cursorEntry = mode === 'oauth' ? oauth : mode === 'remote' ? remote : stdio;
157
+ results.push({ host: 'cursor', path: mergeCursorConfig(cursorEntry), written: true });
158
+ const vscodeEntry = mode === 'oauth' ? oauth : mode === 'remote' ? remote : stdio;
159
+ const vscodeTransport = mode === 'stdio' ? 'stdio' : 'http';
160
+ try {
161
+ results.push({
162
+ host: 'vscode',
163
+ path: mergeVsCodeConfig(vscodeEntry, vscodeTransport),
164
+ written: true,
165
+ });
166
+ results.push({
167
+ host: 'vscode',
168
+ path: mergeVsCodeWorkspaceConfig(vscodeEntry, vscodeTransport, projectRoot),
169
+ written: true,
170
+ });
171
+ }
172
+ catch {
173
+ results.push({ host: 'vscode', path: vscodeUserMcpPath(), written: false });
174
+ }
175
+ if (mode === 'stdio') {
176
+ try {
177
+ results.push({
178
+ host: 'claude-desktop',
179
+ path: mergeClaudeDesktopConfig(stdio),
180
+ written: true,
181
+ });
182
+ }
183
+ catch {
184
+ results.push({ host: 'claude-desktop', path: claudeDesktopConfigPath(), written: false });
185
+ }
186
+ }
187
+ if (mode === 'oauth' || mode === 'remote') {
188
+ try {
189
+ results.push({
190
+ host: 'github',
191
+ path: mergeGitHubConfig(vscodeEntry, vscodeTransport, projectRoot),
192
+ written: true,
193
+ });
194
+ }
195
+ catch {
196
+ results.push({ host: 'github', path: path.join(projectRoot, '.github', 'mcp.json'), written: false });
197
+ }
198
+ }
199
+ return results;
200
+ }
@@ -0,0 +1,9 @@
1
+ /** Decode GPS JWT claims without verification — backend validates on API calls. */
2
+ export interface GpsJwtClaims {
3
+ sub?: string;
4
+ lpId?: string;
5
+ role?: string;
6
+ aud?: string | string[];
7
+ scope?: string;
8
+ }
9
+ export declare function decodeGpsJwt(token: string): GpsJwtClaims;
@@ -0,0 +1,12 @@
1
+ export function decodeGpsJwt(token) {
2
+ const parts = token.split('.');
3
+ if (parts.length < 2)
4
+ return {};
5
+ try {
6
+ const json = Buffer.from(parts[1], 'base64url').toString('utf8');
7
+ return JSON.parse(json);
8
+ }
9
+ catch {
10
+ return {};
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ export interface PkcePair {
2
+ codeVerifier: string;
3
+ codeChallenge: string;
4
+ }
5
+ export declare function generatePkce(): PkcePair;
6
+ export declare function randomState(): string;
@@ -0,0 +1,11 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ export function generatePkce() {
3
+ const codeVerifier = randomBytes(32).toString('base64url');
4
+ const codeChallenge = createHash('sha256')
5
+ .update(codeVerifier)
6
+ .digest('base64url');
7
+ return { codeVerifier, codeChallenge };
8
+ }
9
+ export function randomState() {
10
+ return randomBytes(16).toString('base64url');
11
+ }
@@ -0,0 +1,14 @@
1
+ import type { GpsJwtClaims } from './jwt.js';
2
+ export declare const MCP_SCOPE = "mcp:read";
3
+ export interface McpTokenPolicyOptions {
4
+ /** Expected resource URL, e.g. https://lp.gpsglobal.ai/mcp */
5
+ resourceUrl: string;
6
+ /** Reject tokens whose aud claim does not match resourceUrl */
7
+ strictAudience?: boolean;
8
+ }
9
+ /**
10
+ * Token passthrough audit (docs/57-mcp/004-auth-and-security.md):
11
+ * - MCP HTTP validates aud + scope + role before proxying to backend
12
+ * - Never forwards unvalidated tokens; backend re-validates JWT signature
13
+ */
14
+ export declare function assertMcpHttpToken(claims: GpsJwtClaims, opts: McpTokenPolicyOptions): void;
@@ -0,0 +1,27 @@
1
+ export const MCP_SCOPE = 'mcp:read';
2
+ /**
3
+ * Token passthrough audit (docs/57-mcp/004-auth-and-security.md):
4
+ * - MCP HTTP validates aud + scope + role before proxying to backend
5
+ * - Never forwards unvalidated tokens; backend re-validates JWT signature
6
+ */
7
+ export function assertMcpHttpToken(claims, opts) {
8
+ if (!claims.lpId) {
9
+ throw new Error('Token missing lpId claim');
10
+ }
11
+ const aud = claims.aud;
12
+ const normalizedResource = opts.resourceUrl.replace(/\/+$/, '');
13
+ if (aud) {
14
+ const audValue = Array.isArray(aud) ? aud[0] : aud;
15
+ const normalizedAud = String(audValue).replace(/\/+$/, '');
16
+ if (normalizedAud !== normalizedResource) {
17
+ throw new Error(`Token audience mismatch (expected ${normalizedResource})`);
18
+ }
19
+ }
20
+ else if (opts.strictAudience) {
21
+ throw new Error('Token missing aud claim — use OAuth Connect or npx gpsglobal login --browser');
22
+ }
23
+ const scope = claims.scope;
24
+ if (scope && !String(scope).includes(MCP_SCOPE)) {
25
+ throw new Error(`Token missing required scope ${MCP_SCOPE}`);
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { GpsApiClient } from '../clients/gps-api.js';
3
+ export declare function createGpsMcpServer(client: GpsApiClient): McpServer;
@@ -0,0 +1,9 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerGpsTools } from '../tools/register.js';
3
+ const SERVER_NAME = process.env.MCP_SERVER_NAME ?? 'gpsglobal';
4
+ const SERVER_VERSION = process.env.MCP_SERVER_VERSION ?? '1.4.0';
5
+ export function createGpsMcpServer(client) {
6
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
7
+ registerGpsTools(server, client);
8
+ return server;
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { GpsApiClient } from '../clients/gps-api.js';
3
+ export declare function registerGpsTools(server: McpServer, client: GpsApiClient): void;