@gpsglobal-ai/gpsglobal 1.4.5 → 1.4.7
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 +12 -0
- package/README.md +1 -1
- package/dist/lib/host-config.d.ts +1 -1
- package/dist/lib/host-config.js +1 -8
- package/dist/lib/nmg-lineage.d.ts +14 -0
- package/dist/lib/nmg-lineage.js +34 -0
- package/dist/lib/public-url.d.ts +14 -0
- package/dist/lib/public-url.js +30 -0
- package/dist/lib/wiki-linked.d.ts +37 -0
- package/dist/lib/wiki-linked.js +96 -0
- package/dist/lib/wiki-serve.d.ts +17 -0
- package/dist/lib/wiki-serve.js +47 -0
- package/dist/tools/register.js +21 -34
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog — gpsglobal
|
|
2
2
|
|
|
3
|
+
## 1.4.7 — 2026-06-21
|
|
4
|
+
|
|
5
|
+
- **README** — document `get_fund_wiki` `format=linked` default (PDF page deep links)
|
|
6
|
+
- **Docs sync** — `docs/57-mcp/` index, client config, production proof updated to v1.4.6+
|
|
7
|
+
|
|
8
|
+
## 1.4.6 — 2026-06-21
|
|
9
|
+
|
|
10
|
+
- **`get_fund_wiki` default `format=linked`** — `{P18}` → markdown link to LP Workspace `?tab=analyze#page=18`
|
|
11
|
+
- **`format=raw`** — byte-identical NMG wiki.md (MCP-17 parity); **`format=summary`** unchanged
|
|
12
|
+
- Edge cases: multi-page `{P3, P7}`, conflict `{P3≠P7}`, divider pages (no link), page clamp to `extracted_pages`
|
|
13
|
+
- Docs: [`docs/57-mcp/020-wiki-linked-citations.md`](../docs/57-mcp/020-wiki-linked-citations.md)
|
|
14
|
+
|
|
3
15
|
## 1.4.5 — 2026-06-21
|
|
4
16
|
|
|
5
17
|
- **Production defaults:** `setup` and `login` use `https://lp.gpsglobal.ai` (not `localhost:8080`)
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ This package implements a [Model Context Protocol (MCP)](https://modelcontextpro
|
|
|
13
13
|
| Tool | Description |
|
|
14
14
|
|------|-------------|
|
|
15
15
|
| `list_funds` | List funds in your LP vault (with `has_wiki` metadata) |
|
|
16
|
-
| `get_fund_wiki` | Return
|
|
16
|
+
| `get_fund_wiki` | Return fund wiki markdown. **Default `format=linked`** turns `{P18}` citations into clickable PDF page links (`?tab=analyze#page=18`). Use `format=raw` for byte-identical on-disk wiki.md. |
|
|
17
17
|
|
|
18
18
|
Data never leaves GPS infrastructure except through your authenticated session — the MCP proxies to `https://lp.gpsglobal.ai` with LP-scoped JWT or OAuth tokens.
|
|
19
19
|
|
|
@@ -40,7 +40,7 @@ export declare function mergeCursorConfig(entry: StdioEntry | RemoteEntry, serve
|
|
|
40
40
|
export declare function mergeVsCodeConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http'): string;
|
|
41
41
|
export declare function mergeVsCodeWorkspaceConfig(entry: StdioEntry | RemoteEntry, transport?: 'stdio' | 'http', projectRoot?: string): string;
|
|
42
42
|
export declare function mergeClaudeDesktopConfig(entry: StdioEntry, serverKey?: string): string;
|
|
43
|
-
export
|
|
43
|
+
export { resolveMcpPublicUrl } from './public-url.js';
|
|
44
44
|
export declare function claudeCodeAddCommand(mcpPublicUrl: string, mode?: 'oauth' | 'remote'): string;
|
|
45
45
|
export interface SetupHostResult {
|
|
46
46
|
host: HostId;
|
package/dist/lib/host-config.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { DEFAULT_GPS_API_BASE } from '../config/env.js';
|
|
9
8
|
/** npm scoped package under org gpsglobal-ai (see docs/57-mcp/018-npm-publish-and-cicd.md). */
|
|
10
9
|
export const NPM_PACKAGE = '@gpsglobal-ai/gpsglobal';
|
|
11
10
|
/** MCP protocol server key in mcp.json — short, stable across hosts. */
|
|
@@ -129,13 +128,7 @@ export function mergeVsCodeWorkspaceConfig(entry, transport = 'stdio', projectRo
|
|
|
129
128
|
export function mergeClaudeDesktopConfig(entry, serverKey = MCP_SERVER_KEY) {
|
|
130
129
|
return mergeJsonConfig(claudeDesktopConfigPath(), 'mcpServers', serverKey, entry);
|
|
131
130
|
}
|
|
132
|
-
export
|
|
133
|
-
const normalized = apiBase.replace(/\/+$/, '');
|
|
134
|
-
if (normalized.includes('localhost') || normalized.includes('127.0.0.1')) {
|
|
135
|
-
return process.env.GPS_MCP_PUBLIC_URL ?? 'http://localhost:3100';
|
|
136
|
-
}
|
|
137
|
-
return process.env.GPS_MCP_PUBLIC_URL ?? (normalized || DEFAULT_GPS_API_BASE);
|
|
138
|
-
}
|
|
131
|
+
export { resolveMcpPublicUrl } from './public-url.js';
|
|
139
132
|
export function claudeCodeAddCommand(mcpPublicUrl, mode = 'oauth') {
|
|
140
133
|
const url = `${mcpPublicUrl.replace(/\/+$/, '')}/mcp`;
|
|
141
134
|
if (mode === 'oauth') {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NMG lineage token helpers — mirrors ml-service/services/nmg.py LINEAGE_RE.
|
|
3
|
+
* Spec: docs/05-incidents/01-citation-navigation-red-team/06-references/08-wiki-format.md
|
|
4
|
+
*/
|
|
5
|
+
/** Match `{P3}`, `{P3, P7}`, `{P3≠P7}` (whitespace tolerant). */
|
|
6
|
+
export declare const LINEAGE_RE: RegExp;
|
|
7
|
+
export interface ParsedLineage {
|
|
8
|
+
raw: string;
|
|
9
|
+
pages: number[];
|
|
10
|
+
conflicting: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseLineageToken(token: string): ParsedLineage | null;
|
|
13
|
+
export declare function findLineageTokens(text: string): ParsedLineage[];
|
|
14
|
+
export declare function clampPage(page: number, maxPage?: number): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NMG lineage token helpers — mirrors ml-service/services/nmg.py LINEAGE_RE.
|
|
3
|
+
* Spec: docs/05-incidents/01-citation-navigation-red-team/06-references/08-wiki-format.md
|
|
4
|
+
*/
|
|
5
|
+
/** Match `{P3}`, `{P3, P7}`, `{P3≠P7}` (whitespace tolerant). */
|
|
6
|
+
export const LINEAGE_RE = /\{\s*P(\d+)((?:\s*[,≠]\s*P\d+)*)\s*\}/g;
|
|
7
|
+
const PAGE_NUM_RE = /P(\d+)/g;
|
|
8
|
+
export function parseLineageToken(token) {
|
|
9
|
+
const m = token.match(/^\{\s*P(\d+)((?:\s*[,≠]\s*P\d+)*)\s*\}$/);
|
|
10
|
+
if (!m)
|
|
11
|
+
return null;
|
|
12
|
+
const inner = m[0];
|
|
13
|
+
const conflicting = inner.includes('≠');
|
|
14
|
+
const pages = [...inner.matchAll(PAGE_NUM_RE)]
|
|
15
|
+
.map((x) => Number(x[1]))
|
|
16
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
17
|
+
return { raw: inner, pages, conflicting };
|
|
18
|
+
}
|
|
19
|
+
export function findLineageTokens(text) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const m of text.matchAll(LINEAGE_RE)) {
|
|
22
|
+
const parsed = parseLineageToken(m[0]);
|
|
23
|
+
if (parsed)
|
|
24
|
+
out.push(parsed);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export function clampPage(page, maxPage) {
|
|
29
|
+
if (!Number.isFinite(page) || page < 1)
|
|
30
|
+
return 1;
|
|
31
|
+
if (maxPage != null && maxPage > 0)
|
|
32
|
+
return Math.min(page, maxPage);
|
|
33
|
+
return page;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** LP Workspace deep-link path (FundDetailPage + Event Browser artifacts). */
|
|
2
|
+
export declare const FUND_ANALYZE_DEEP_LINK_PATTERN = "/lp/{lpId}/fund/{fundId}?tab=analyze#page={N}";
|
|
3
|
+
export interface WikiLinkedContext {
|
|
4
|
+
lpId: string;
|
|
5
|
+
fundId: string;
|
|
6
|
+
/** Public LP Workspace origin, e.g. https://lp.gpsglobal.ai or http://localhost:5173 */
|
|
7
|
+
workspacePublicUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function isLocalHostOrigin(url: string): boolean;
|
|
10
|
+
/** Public MCP Streamable HTTP origin (e.g. https://lp.gpsglobal.ai or http://localhost:3100). */
|
|
11
|
+
export declare function resolveMcpPublicUrl(apiBase: string): string;
|
|
12
|
+
/** Public LP Workspace PWA origin for fund PDF page links (e.g. https://lp.gpsglobal.ai or :5173). */
|
|
13
|
+
export declare function resolveWorkspacePublicUrl(apiBase: string): string;
|
|
14
|
+
export declare function buildFundAnalyzePageUrl(workspacePublicUrl: string, lpId: string, fundId: string, page: number): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public origin resolution for MCP clients — single place for prod vs local URLs.
|
|
3
|
+
* MCP HTTP (/mcp), LP Workspace PWA (/lp/…), and API base may differ in local Docker.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_GPS_API_BASE, normalizeApiBase } from '../config/env.js';
|
|
6
|
+
/** LP Workspace deep-link path (FundDetailPage + Event Browser artifacts). */
|
|
7
|
+
export const FUND_ANALYZE_DEEP_LINK_PATTERN = '/lp/{lpId}/fund/{fundId}?tab=analyze#page={N}';
|
|
8
|
+
export function isLocalHostOrigin(url) {
|
|
9
|
+
const normalized = normalizeApiBase(url);
|
|
10
|
+
return normalized.includes('localhost') || normalized.includes('127.0.0.1');
|
|
11
|
+
}
|
|
12
|
+
/** Public MCP Streamable HTTP origin (e.g. https://lp.gpsglobal.ai or http://localhost:3100). */
|
|
13
|
+
export function resolveMcpPublicUrl(apiBase) {
|
|
14
|
+
if (isLocalHostOrigin(apiBase)) {
|
|
15
|
+
return process.env.GPS_MCP_PUBLIC_URL ?? 'http://localhost:3100';
|
|
16
|
+
}
|
|
17
|
+
const normalized = normalizeApiBase(apiBase);
|
|
18
|
+
return process.env.GPS_MCP_PUBLIC_URL ?? (normalized || DEFAULT_GPS_API_BASE);
|
|
19
|
+
}
|
|
20
|
+
/** Public LP Workspace PWA origin for fund PDF page links (e.g. https://lp.gpsglobal.ai or :5173). */
|
|
21
|
+
export function resolveWorkspacePublicUrl(apiBase) {
|
|
22
|
+
if (isLocalHostOrigin(apiBase)) {
|
|
23
|
+
return process.env.GPS_WORKSPACE_PUBLIC_URL ?? 'http://localhost:5173';
|
|
24
|
+
}
|
|
25
|
+
return process.env.GPS_MCP_PUBLIC_URL ?? DEFAULT_GPS_API_BASE;
|
|
26
|
+
}
|
|
27
|
+
export function buildFundAnalyzePageUrl(workspacePublicUrl, lpId, fundId, page) {
|
|
28
|
+
const base = normalizeApiBase(workspacePublicUrl);
|
|
29
|
+
return `${base}/lp/${encodeURIComponent(lpId)}/fund/${encodeURIComponent(fundId)}?tab=analyze#page=${page}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform NMG wiki `{Pn}` lineage tokens into LP Workspace deep links.
|
|
3
|
+
*
|
|
4
|
+
* Deep-link convention: see public-url.ts / docs/57-mcp/020-wiki-linked-citations.md
|
|
5
|
+
* Canonical wiki.md on disk stays unchanged — transform at MCP read time only.
|
|
6
|
+
*/
|
|
7
|
+
import { type WikiLinkedContext } from './public-url.js';
|
|
8
|
+
export type { WikiLinkedContext } from './public-url.js';
|
|
9
|
+
export type WikiServeFormat = 'linked' | 'raw' | 'summary';
|
|
10
|
+
export interface WikiLinkedMeta {
|
|
11
|
+
format: 'linked';
|
|
12
|
+
divider_pages: number[];
|
|
13
|
+
extracted_pages?: number;
|
|
14
|
+
lineage_tokens_transformed: number;
|
|
15
|
+
links_emitted: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SplitWiki {
|
|
18
|
+
frontMatterBlock: string;
|
|
19
|
+
body: string;
|
|
20
|
+
dividerPages: number[];
|
|
21
|
+
extractedPages?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Split YAML front matter; parse divider_pages + extracted_pages for edge-case handling. */
|
|
24
|
+
export declare function splitWikiFrontMatter(raw: string): SplitWiki;
|
|
25
|
+
/** Transform `{Pn}` tokens in wiki body to markdown page links. Front matter is preserved verbatim. */
|
|
26
|
+
export declare function transformWikiBodyWithPageLinks(body: string, ctx: WikiLinkedContext, opts?: {
|
|
27
|
+
dividerPages?: number[];
|
|
28
|
+
extractedPages?: number;
|
|
29
|
+
}): {
|
|
30
|
+
text: string;
|
|
31
|
+
tokensTransformed: number;
|
|
32
|
+
linksEmitted: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function transformWikiWithPageLinks(rawWiki: string, ctx: WikiLinkedContext): {
|
|
35
|
+
content: string;
|
|
36
|
+
meta: WikiLinkedMeta;
|
|
37
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform NMG wiki `{Pn}` lineage tokens into LP Workspace deep links.
|
|
3
|
+
*
|
|
4
|
+
* Deep-link convention: see public-url.ts / docs/57-mcp/020-wiki-linked-citations.md
|
|
5
|
+
* Canonical wiki.md on disk stays unchanged — transform at MCP read time only.
|
|
6
|
+
*/
|
|
7
|
+
import { buildFundAnalyzePageUrl, } from './public-url.js';
|
|
8
|
+
import { clampPage, LINEAGE_RE, parseLineageToken } from './nmg-lineage.js';
|
|
9
|
+
/** Split YAML front matter; parse divider_pages + extracted_pages for edge-case handling. */
|
|
10
|
+
export function splitWikiFrontMatter(raw) {
|
|
11
|
+
const fm = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
12
|
+
if (!fm) {
|
|
13
|
+
return { frontMatterBlock: '', body: raw, dividerPages: [], extractedPages: undefined };
|
|
14
|
+
}
|
|
15
|
+
const yaml = fm[1];
|
|
16
|
+
const dividerPages = parseYamlIntList(yaml, 'divider_pages');
|
|
17
|
+
const extractedPages = parseYamlInt(yaml, 'extracted_pages');
|
|
18
|
+
return {
|
|
19
|
+
frontMatterBlock: fm[0],
|
|
20
|
+
body: raw.slice(fm[0].length),
|
|
21
|
+
dividerPages,
|
|
22
|
+
extractedPages,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseYamlIntList(yaml, key) {
|
|
26
|
+
const m = yaml.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, 'm'));
|
|
27
|
+
if (!m)
|
|
28
|
+
return [];
|
|
29
|
+
return m[1]
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
32
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
33
|
+
}
|
|
34
|
+
function parseYamlInt(yaml, key) {
|
|
35
|
+
const m = yaml.match(new RegExp(`^${key}:\\s*(\\d+)`, 'm'));
|
|
36
|
+
if (!m)
|
|
37
|
+
return undefined;
|
|
38
|
+
const n = parseInt(m[1], 10);
|
|
39
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
40
|
+
}
|
|
41
|
+
function renderPageLink(page, ctx, opts) {
|
|
42
|
+
if (opts.dividerPages.includes(page)) {
|
|
43
|
+
return `[Page ${page} (divider — no document content)](#gps-divider-page-${page})`;
|
|
44
|
+
}
|
|
45
|
+
const clamped = clampPage(page, opts.maxPage);
|
|
46
|
+
const label = clamped !== page ? `Page ${page}→${clamped}` : `Page ${clamped}`;
|
|
47
|
+
return `[${label}](${buildFundAnalyzePageUrl(ctx.workspacePublicUrl, ctx.lpId, ctx.fundId, clamped)})`;
|
|
48
|
+
}
|
|
49
|
+
function renderLineageLinks(parsed, ctx, opts) {
|
|
50
|
+
if (parsed.pages.length === 0)
|
|
51
|
+
return parsed.raw;
|
|
52
|
+
if (parsed.conflicting) {
|
|
53
|
+
return parsed.pages.map((p) => renderPageLink(p, ctx, opts)).join(' **vs** ');
|
|
54
|
+
}
|
|
55
|
+
if (parsed.pages.length === 1) {
|
|
56
|
+
return renderPageLink(parsed.pages[0], ctx, opts);
|
|
57
|
+
}
|
|
58
|
+
return parsed.pages.map((p) => renderPageLink(p, ctx, opts)).join(', ');
|
|
59
|
+
}
|
|
60
|
+
/** Transform `{Pn}` tokens in wiki body to markdown page links. Front matter is preserved verbatim. */
|
|
61
|
+
export function transformWikiBodyWithPageLinks(body, ctx, opts = {}) {
|
|
62
|
+
const linkOpts = {
|
|
63
|
+
dividerPages: opts.dividerPages ?? [],
|
|
64
|
+
maxPage: opts.extractedPages,
|
|
65
|
+
};
|
|
66
|
+
let tokensTransformed = 0;
|
|
67
|
+
let linksEmitted = 0;
|
|
68
|
+
const text = body.replace(LINEAGE_RE, (match) => {
|
|
69
|
+
const parsed = parseLineageToken(match);
|
|
70
|
+
if (!parsed)
|
|
71
|
+
return match;
|
|
72
|
+
tokensTransformed += 1;
|
|
73
|
+
linksEmitted += parsed.pages.filter((p) => !linkOpts.dividerPages.includes(p)).length;
|
|
74
|
+
return renderLineageLinks(parsed, ctx, linkOpts);
|
|
75
|
+
});
|
|
76
|
+
return { text, tokensTransformed, linksEmitted };
|
|
77
|
+
}
|
|
78
|
+
export function transformWikiWithPageLinks(rawWiki, ctx) {
|
|
79
|
+
const split = splitWikiFrontMatter(rawWiki);
|
|
80
|
+
const { text, tokensTransformed, linksEmitted } = transformWikiBodyWithPageLinks(split.body, ctx, { dividerPages: split.dividerPages, extractedPages: split.extractedPages });
|
|
81
|
+
const preamble = [
|
|
82
|
+
'> **GPS linked wiki** — citation links open **Analyze → PDF** at the cited page in LP Workspace.',
|
|
83
|
+
'> Requires GPS sign-in. Canonical NMG tokens `{Pn}` on disk; request `format=raw` for byte-identical wiki.md.',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n');
|
|
86
|
+
return {
|
|
87
|
+
content: split.frontMatterBlock + preamble + text,
|
|
88
|
+
meta: {
|
|
89
|
+
format: 'linked',
|
|
90
|
+
divider_pages: split.dividerPages,
|
|
91
|
+
extracted_pages: split.extractedPages,
|
|
92
|
+
lineage_tokens_transformed: tokensTransformed,
|
|
93
|
+
links_emitted: linksEmitted,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type WikiLinkedMeta, type WikiServeFormat } from './wiki-linked.js';
|
|
2
|
+
export interface ServeFundWikiInput {
|
|
3
|
+
content: string;
|
|
4
|
+
fundId: string;
|
|
5
|
+
lpId: string;
|
|
6
|
+
fundName: string;
|
|
7
|
+
wikiStatus: string;
|
|
8
|
+
format?: WikiServeFormat;
|
|
9
|
+
}
|
|
10
|
+
export interface ServeFundWikiResult {
|
|
11
|
+
body: string;
|
|
12
|
+
format: WikiServeFormat;
|
|
13
|
+
linkedMeta?: WikiLinkedMeta;
|
|
14
|
+
}
|
|
15
|
+
/** Transform or pass through wiki markdown per format. */
|
|
16
|
+
export declare function serveFundWiki(input: ServeFundWikiInput): ServeFundWikiResult;
|
|
17
|
+
export declare function buildFundWikiStructuredContent(input: ServeFundWikiInput, served: ServeFundWikiResult): Record<string, unknown>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve fund wiki content in the requested format — SRP between tool registration and transform.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveDefaultApiBase } from '../config/env.js';
|
|
5
|
+
import { FUND_ANALYZE_DEEP_LINK_PATTERN, resolveWorkspacePublicUrl } from './public-url.js';
|
|
6
|
+
import { splitWikiFrontMatter, transformWikiWithPageLinks, } from './wiki-linked.js';
|
|
7
|
+
function wikiSummary(content) {
|
|
8
|
+
const { frontMatterBlock, body } = splitWikiFrontMatter(content);
|
|
9
|
+
const titles = body.split('\n').filter((l) => l.startsWith('#')).slice(0, 20);
|
|
10
|
+
if (!frontMatterBlock)
|
|
11
|
+
return titles.join('\n').trim();
|
|
12
|
+
return `${frontMatterBlock}${titles.join('\n')}`.trim();
|
|
13
|
+
}
|
|
14
|
+
function extractSchemaVersion(content) {
|
|
15
|
+
const m = content.match(/schema_version:\s*["']?([^"'\n]+)/);
|
|
16
|
+
return m?.[1]?.trim();
|
|
17
|
+
}
|
|
18
|
+
/** Transform or pass through wiki markdown per format. */
|
|
19
|
+
export function serveFundWiki(input) {
|
|
20
|
+
const format = input.format ?? 'linked';
|
|
21
|
+
if (format === 'summary') {
|
|
22
|
+
return { body: wikiSummary(input.content), format };
|
|
23
|
+
}
|
|
24
|
+
if (format === 'raw') {
|
|
25
|
+
return { body: input.content, format };
|
|
26
|
+
}
|
|
27
|
+
const transformed = transformWikiWithPageLinks(input.content, {
|
|
28
|
+
lpId: input.lpId,
|
|
29
|
+
fundId: input.fundId,
|
|
30
|
+
workspacePublicUrl: resolveWorkspacePublicUrl(resolveDefaultApiBase()),
|
|
31
|
+
});
|
|
32
|
+
return { body: transformed.content, format, linkedMeta: transformed.meta };
|
|
33
|
+
}
|
|
34
|
+
export function buildFundWikiStructuredContent(input, served) {
|
|
35
|
+
return {
|
|
36
|
+
fund_id: input.fundId,
|
|
37
|
+
fund_name: input.fundName,
|
|
38
|
+
has_wiki: true,
|
|
39
|
+
wiki_status: input.wikiStatus,
|
|
40
|
+
format: served.format,
|
|
41
|
+
schema_version: extractSchemaVersion(input.content),
|
|
42
|
+
byte_length_raw: input.content.length,
|
|
43
|
+
byte_length: served.body.length,
|
|
44
|
+
pdf_deep_link_pattern: FUND_ANALYZE_DEEP_LINK_PATTERN,
|
|
45
|
+
...(served.linkedMeta ?? {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/tools/register.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { assertMcpEligibleRole } from '../config/env.js';
|
|
3
|
+
import { buildFundWikiStructuredContent, serveFundWiki } from '../lib/wiki-serve.js';
|
|
3
4
|
const listFundsSchema = {
|
|
4
5
|
include_closed: z.boolean().optional().describe('If false (default), omit funds whose closing_date is in the past.'),
|
|
5
6
|
};
|
|
6
7
|
const getFundWikiSchema = {
|
|
7
8
|
fund_id: z.string().describe('Fund identifier from list_funds (e.g. fund_1776212451730_0).'),
|
|
8
|
-
format: z
|
|
9
|
+
format: z
|
|
10
|
+
.enum(['linked', 'raw', 'summary'])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('linked (default) = {Pn} → clickable LP Workspace PDF page links; '
|
|
13
|
+
+ 'raw = byte-identical NMG wiki.md from vault; summary = YAML header + section titles only.'),
|
|
9
14
|
};
|
|
10
15
|
function isClosed(closingDate) {
|
|
11
16
|
if (!closingDate)
|
|
@@ -32,28 +37,6 @@ function mapFund(f) {
|
|
|
32
37
|
fund_summary: f.fundSummary ?? '',
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
|
-
function wikiSummary(content) {
|
|
36
|
-
const lines = content.split('\n');
|
|
37
|
-
const header = [];
|
|
38
|
-
let inYaml = false;
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
if (line.trim() === '---') {
|
|
41
|
-
inYaml = !inYaml;
|
|
42
|
-
header.push(line);
|
|
43
|
-
if (!inYaml && header.length > 1)
|
|
44
|
-
break;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (inYaml)
|
|
48
|
-
header.push(line);
|
|
49
|
-
}
|
|
50
|
-
const titles = lines.filter((l) => l.startsWith('#')).slice(0, 20);
|
|
51
|
-
return [...header, '', ...titles].join('\n').trim();
|
|
52
|
-
}
|
|
53
|
-
function extractSchemaVersion(content) {
|
|
54
|
-
const m = content.match(/schema_version:\s*["']?([^"'\n]+)/);
|
|
55
|
-
return m?.[1]?.trim();
|
|
56
|
-
}
|
|
57
40
|
export function registerGpsTools(server, client) {
|
|
58
41
|
server.registerTool('list_funds', {
|
|
59
42
|
description: 'List all funds in the authenticated LP vault with metadata and has_wiki flags.',
|
|
@@ -74,7 +57,10 @@ export function registerGpsTools(server, client) {
|
|
|
74
57
|
};
|
|
75
58
|
});
|
|
76
59
|
server.registerTool('get_fund_wiki', {
|
|
77
|
-
description: 'Retrieve NMG wiki
|
|
60
|
+
description: 'Retrieve NMG fund wiki for the authenticated LP vault. '
|
|
61
|
+
+ 'Default format=linked transforms {P18} lineage tokens into LP Workspace deep links '
|
|
62
|
+
+ '(?tab=analyze#page=N) so hosts can cite "Page 18" with a clickable PDF anchor. '
|
|
63
|
+
+ 'Use format=raw for canonical on-disk wiki.md (byte parity with API).',
|
|
78
64
|
inputSchema: getFundWikiSchema,
|
|
79
65
|
}, async ({ fund_id, format }) => {
|
|
80
66
|
assertMcpEligibleRole(client.config.role);
|
|
@@ -92,17 +78,18 @@ export function registerGpsTools(server, client) {
|
|
|
92
78
|
isError: true,
|
|
93
79
|
};
|
|
94
80
|
}
|
|
95
|
-
const
|
|
81
|
+
const serveInput = {
|
|
82
|
+
content: wiki.content,
|
|
83
|
+
fundId: fund_id,
|
|
84
|
+
lpId: client.config.lpId,
|
|
85
|
+
fundName: wiki.fund_name,
|
|
86
|
+
wikiStatus: wiki.wiki_status,
|
|
87
|
+
format,
|
|
88
|
+
};
|
|
89
|
+
const served = serveFundWiki(serveInput);
|
|
96
90
|
return {
|
|
97
|
-
content: [{ type: 'text', text: body }],
|
|
98
|
-
structuredContent:
|
|
99
|
-
fund_id: wiki.fund_id,
|
|
100
|
-
fund_name: wiki.fund_name,
|
|
101
|
-
has_wiki: true,
|
|
102
|
-
wiki_status: wiki.wiki_status,
|
|
103
|
-
schema_version: extractSchemaVersion(wiki.content),
|
|
104
|
-
byte_length: wiki.content.length,
|
|
105
|
-
},
|
|
91
|
+
content: [{ type: 'text', text: served.body }],
|
|
92
|
+
structuredContent: buildFundWikiStructuredContent(serveInput, served),
|
|
106
93
|
};
|
|
107
94
|
});
|
|
108
95
|
}
|