fuli-context 0.5.0 → 0.7.2
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.en.md +2 -333
- package/README.md +370 -202
- package/README.zh-CN.md +448 -0
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
- package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
- package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
- package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
- package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-CbDsKrsO.js +1 -0
- package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
- package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
- package/dist/web/assets/i18n-BQcns1PH.js +5 -0
- package/dist/web/assets/identity-B8K4VYg6.js +1 -0
- package/dist/web/assets/index-CB8Xmsc2.js +2 -0
- package/dist/web/assets/index-CZeuPh-U.css +1 -0
- package/dist/web/assets/model-eF52GgPF.js +1 -0
- package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
- package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
- package/dist/web/index.html +9 -6
- package/docs/external-knowledge-architecture.md +279 -0
- package/examples/external-knowledge/markdown-folder.mjs +235 -0
- package/graph-provider/fuli_graph/app.py +57 -1
- package/graph-provider/fuli_graph/graph_models.py +3 -0
- package/graph-provider/fuli_graph/graph_query.py +62 -16
- package/graph-provider/fuli_graph/knowledge_review.py +612 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
- package/graph-provider/fuli_graph/knowledge_search.py +51 -4
- package/graph-provider/fuli_graph/models.py +19 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +5 -407
- package/package.json +7 -5
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +73 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +79 -4
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +51 -28
- package/src/cli/local-runtime-options.js +14 -8
- package/src/cli/setup-agent-selection.js +12 -12
- package/src/cli/setup-command.js +41 -18
- package/src/cli/shell-argument.js +10 -0
- package/src/cli/uninstall-command.js +17 -15
- package/src/cli/update-command.js +38 -30
- package/src/external-knowledge/catalog.js +61 -0
- package/src/external-knowledge/conflict-policy.js +58 -0
- package/src/external-knowledge/connected-search.js +166 -0
- package/src/external-knowledge/connectors/custom.js +133 -0
- package/src/external-knowledge/connectors/feishu.js +461 -0
- package/src/external-knowledge/connectors/mcp.js +438 -0
- package/src/external-knowledge/connectors/notion.js +307 -0
- package/src/external-knowledge/connectors/retrieval-api.js +222 -0
- package/src/external-knowledge/document-mapping.js +151 -0
- package/src/external-knowledge/graph-projection.js +17 -0
- package/src/external-knowledge/index.js +8 -0
- package/src/external-knowledge/registry.js +91 -0
- package/src/external-knowledge/runtime.js +68 -0
- package/src/external-knowledge/safe-diagnostic.js +14 -0
- package/src/external-knowledge/service.js +811 -0
- package/src/graphiti/agent-knowledge-workflows.js +7 -2
- package/src/graphiti/federated-application.js +66 -41
- package/src/graphiti/knowledge-item-role.js +21 -0
- package/src/graphiti/knowledge-review.js +43 -0
- package/src/graphiti/project-profile-mapping.js +32 -0
- package/src/graphiti/provider-client.js +22 -1
- package/src/graphiti/source-marker.js +54 -3
- package/src/graphiti/task-knowledge-recall.js +250 -0
- package/src/http/api-router.js +23 -108
- package/src/http/external-knowledge-api-router.js +104 -0
- package/src/http/graph-api-router.js +12 -1
- package/src/http/request-policy.js +1 -1
- package/src/local-runtime/lifecycle.js +26 -15
- package/src/mcp/create-mcp-server.js +1 -0
- package/src/mcp/instructions.js +7 -7
- package/src/mcp/tool-annotations.js +6 -0
- package/src/mcp/tool-result.js +1 -0
- package/src/server/application-lifecycle.js +2 -11
- package/src/server/blocked-ports.js +11 -0
- package/src/server.js +36 -17
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +2 -1
- package/src/setup/claude-code-config.js +2 -1
- package/src/setup/codex-bootstrap.js +12 -7
- package/src/setup/container-runtime.js +25 -21
- package/src/setup/graph-runtime.js +148 -56
- package/src/setup/node-runtime.js +3 -3
- package/src/setup/options.js +2 -4
- package/src/setup/paths.js +18 -3
- package/src/setup/review-skill-adapters.js +49 -0
- package/src/setup/setup.js +26 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +317 -0
- package/src/system/runtime-settings.js +131 -0
- package/src/system/system-service.js +119 -0
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
- package/dist/web/assets/console-B_Ena2DS.js +0 -5
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-CjBwYSMX.js +0 -1
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- package/dist/web/assets/index-DAzeFJSc.js +0 -2
- package/dist/web/assets/model-C-5RVIJ2.js +0 -1
- package/dist/web/assets/paths-BkULqmGI.js +0 -1
- package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
- package/src/agent/candidate-read-service.js +0 -53
- package/src/agent/context-pack-service.js +0 -83
- package/src/agent/public-read-policy.js +0 -39
- package/src/agent/public-read-service.js +0 -69
- package/src/agent/read-projection.js +0 -107
- package/src/agent/search-service.js +0 -61
- package/src/agent/space-boundary.js +0 -33
- package/src/app/bootstrap.js +0 -8
- package/src/app/create-application.js +0 -143
- package/src/app/state-service.js +0 -38
- package/src/candidates.js +0 -78
- package/src/classifier.js +0 -63
- package/src/cli/command-arguments.js +0 -14
- package/src/context-pack.js +0 -206
- package/src/extractor.js +0 -77
- package/src/fact-history.js +0 -39
- package/src/facts/fact-service.js +0 -89
- package/src/http/bounded-json-array.js +0 -19
- package/src/http/lens-route.js +0 -50
- package/src/ingestion.js +0 -161
- package/src/lens/lens-correction-service.js +0 -81
- package/src/lens/lens-fact-writer.js +0 -85
- package/src/lens/lens-input.js +0 -62
- package/src/lens/lens-query-projection.js +0 -87
- package/src/lens/lens-query-ranking.js +0 -62
- package/src/lens/lens-query.js +0 -143
- package/src/lens/lens-resource-projection.js +0 -93
- package/src/lens/lens-resource-selection.js +0 -45
- package/src/lens/lens-resource-service.js +0 -103
- package/src/lens/lens-search-query.js +0 -122
- package/src/lens/lens-service.js +0 -183
- package/src/models.js +0 -44
- package/src/observer.js +0 -83
- package/src/project-rules.js +0 -33
- package/src/publication/outbox-service.js +0 -46
- package/src/publication/publication-envelope-schema.js +0 -75
- package/src/publication/publication-envelope.js +0 -37
- package/src/publication/publication-policy.js +0 -82
- package/src/publication/publication-service.js +0 -48
- package/src/router.js +0 -58
- package/src/runtime-options.js +0 -138
- package/src/security/episode-visibility.js +0 -14
- package/src/security/fact-visibility.js +0 -15
- package/src/security/source-metadata-policy.js +0 -8
- package/src/storage/episode-evidence-preview.js +0 -100
- package/src/storage/fact-search-order.js +0 -18
- package/src/storage/file/episode-evidence.js +0 -37
- package/src/storage/file/episode-index.js +0 -59
- package/src/storage/file/fact-search.js +0 -60
- package/src/storage/file-store.js +0 -437
- package/src/storage/migrate.js +0 -128
- package/src/storage/migrations/001-initial.sql +0 -83
- package/src/storage/migrations/002-storage-integrity.sql +0 -113
- package/src/storage/record-validation.js +0 -46
- package/src/storage/snapshot-defaults.js +0 -54
- package/src/storage/sqlite/candidates-repository.js +0 -84
- package/src/storage/sqlite/episode-evidence-repository.js +0 -154
- package/src/storage/sqlite/episode-privacy-sql.js +0 -33
- package/src/storage/sqlite/episodes-repository.js +0 -52
- package/src/storage/sqlite/fact-search-repository.js +0 -162
- package/src/storage/sqlite/facts-repository.js +0 -146
- package/src/storage/sqlite/imports-repository.js +0 -39
- package/src/storage/sqlite/mapper.js +0 -84
- package/src/storage/sqlite/open-database.js +0 -15
- package/src/storage/sqlite/outbox-repository.js +0 -103
- package/src/storage/sqlite/snapshot-repository.js +0 -69
- package/src/storage/sqlite/spaces-repository.js +0 -52
- package/src/storage/sqlite/subscriptions-repository.js +0 -54
- package/src/storage/sqlite-store.js +0 -159
- package/src/storage/stable-json.js +0 -24
- package/src/storage/store-port.js +0 -20
- package/src/store.js +0 -2
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import {
|
|
3
|
+
StdioClientTransport,
|
|
4
|
+
getDefaultEnvironment
|
|
5
|
+
} from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
|
+
|
|
8
|
+
const LOCAL_CURSOR_PREFIX = 'fuli-mcp-v1:';
|
|
9
|
+
|
|
10
|
+
export function createMcpConnector({ openSession = defaultOpenSession } = {}) {
|
|
11
|
+
return {
|
|
12
|
+
type: 'mcp',
|
|
13
|
+
name: 'MCP',
|
|
14
|
+
capabilities: ['discover', 'sync', 'retrieve'],
|
|
15
|
+
|
|
16
|
+
async check(context) {
|
|
17
|
+
return withSession(openSession, context, async (client) => {
|
|
18
|
+
const capabilities = client.getServerCapabilities?.() ?? {};
|
|
19
|
+
const supported = [];
|
|
20
|
+
if (capabilities.resources) {
|
|
21
|
+
supported.push('discover', 'sync');
|
|
22
|
+
}
|
|
23
|
+
if (capabilities.tools && context.mode !== 'mirror') {
|
|
24
|
+
const searchTool = context.config?.searchTool ?? 'search';
|
|
25
|
+
const fetchTool = context.config?.fetchTool ?? 'fetch';
|
|
26
|
+
const listed = await client.listTools({});
|
|
27
|
+
const tools = listed.tools ?? [];
|
|
28
|
+
assertReadTool(tools, searchTool);
|
|
29
|
+
if (tools.some(({ name }) => name === fetchTool)) {
|
|
30
|
+
assertReadTool(tools, fetchTool);
|
|
31
|
+
}
|
|
32
|
+
supported.push('retrieve');
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
status: 'ready',
|
|
36
|
+
capabilities: supported,
|
|
37
|
+
server: client.getServerVersion?.() ?? null
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async discover(context) {
|
|
43
|
+
return withSession(openSession, context, async (client) => {
|
|
44
|
+
const listed = await resourcePage(client, context);
|
|
45
|
+
return {
|
|
46
|
+
items: listed.resources.map((resource) => ({
|
|
47
|
+
id: resource.uri,
|
|
48
|
+
title: resource.name ?? resource.title ?? resource.uri,
|
|
49
|
+
type: 'resource',
|
|
50
|
+
metadata: { mimeType: resource.mimeType ?? null }
|
|
51
|
+
})),
|
|
52
|
+
nextCursor: listed.nextCursor,
|
|
53
|
+
hasMore: listed.nextCursor !== null
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async sync(context) {
|
|
59
|
+
return withSession(openSession, context, async (client) => {
|
|
60
|
+
const explicitUris = stringArray(context.source?.resourceUris);
|
|
61
|
+
let resources;
|
|
62
|
+
let nextCursor = null;
|
|
63
|
+
if (explicitUris.length) {
|
|
64
|
+
const offset = explicitResourceOffset(context.cursor);
|
|
65
|
+
const limit = connectorLimit(context.limit);
|
|
66
|
+
resources = explicitUris.slice(offset, offset + limit)
|
|
67
|
+
.map((uri) => ({ uri, name: uri }));
|
|
68
|
+
const nextOffset = offset + resources.length;
|
|
69
|
+
nextCursor = nextOffset < explicitUris.length
|
|
70
|
+
? encodeLocalCursor({ mode: 'explicit', offset: nextOffset })
|
|
71
|
+
: null;
|
|
72
|
+
} else {
|
|
73
|
+
const listed = await resourcePage(client, context);
|
|
74
|
+
resources = listed.resources;
|
|
75
|
+
nextCursor = listed.nextCursor;
|
|
76
|
+
}
|
|
77
|
+
const items = [];
|
|
78
|
+
for (const resource of resources) {
|
|
79
|
+
const read = await client.readResource({ uri: resource.uri });
|
|
80
|
+
const content = resourceText(read.contents ?? []);
|
|
81
|
+
if (!content) continue;
|
|
82
|
+
items.push({
|
|
83
|
+
id: resource.uri,
|
|
84
|
+
title: resource.name ?? resource.title ?? resource.uri,
|
|
85
|
+
content,
|
|
86
|
+
url: onlineUrl(resource.uri),
|
|
87
|
+
updatedAt: null,
|
|
88
|
+
metadata: {
|
|
89
|
+
mimeType: resource.mimeType ?? firstMimeType(read.contents) ?? null,
|
|
90
|
+
mcpUri: resource.uri
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
items,
|
|
96
|
+
deleted: [],
|
|
97
|
+
nextCursor,
|
|
98
|
+
hasMore: Boolean(nextCursor)
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async retrieve(context) {
|
|
104
|
+
return withSession(openSession, context, async (client) => {
|
|
105
|
+
const searchTool = context.config?.searchTool ?? 'search';
|
|
106
|
+
const fetchTool = context.config?.fetchTool ?? 'fetch';
|
|
107
|
+
const listed = await client.listTools({});
|
|
108
|
+
const tools = listed.tools ?? [];
|
|
109
|
+
assertReadTool(tools, searchTool);
|
|
110
|
+
const hasFetch = tools.some(({ name }) => name === fetchTool);
|
|
111
|
+
if (hasFetch) assertReadTool(tools, fetchTool);
|
|
112
|
+
const searchResult = await client.callTool({
|
|
113
|
+
name: searchTool,
|
|
114
|
+
arguments: {
|
|
115
|
+
...(plainObject(context.source?.searchArguments) ?? {}),
|
|
116
|
+
[context.config?.searchArgumentName ?? 'query']: context.query
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const candidates = toolItems(searchResult).slice(0, context.limit);
|
|
120
|
+
const items = [];
|
|
121
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
122
|
+
let item = candidate;
|
|
123
|
+
if (!itemContent(item) && hasFetch) {
|
|
124
|
+
const identifier = item?.id ?? item?.uri ?? item?.url;
|
|
125
|
+
if (identifier) {
|
|
126
|
+
const fetched = await client.callTool({
|
|
127
|
+
name: fetchTool,
|
|
128
|
+
arguments: {
|
|
129
|
+
[context.config?.fetchArgumentName ?? 'id']: identifier
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
item = mergeToolItem(item, toolItems(fetched)[0] ?? toolPayload(fetched));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const normalized = normalizeToolItem(item, context.query, index);
|
|
136
|
+
if (normalized) items.push(normalized);
|
|
137
|
+
}
|
|
138
|
+
return { items };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function defaultOpenSession({ config = {}, env = process.env }) {
|
|
145
|
+
const client = new Client({ name: 'fuli-external-knowledge', version: '1.0.0' });
|
|
146
|
+
const transport = config.transport === 'stdio'
|
|
147
|
+
? stdioTransport(config, env)
|
|
148
|
+
: httpTransport(config, env);
|
|
149
|
+
await client.connect(transport);
|
|
150
|
+
let closed = false;
|
|
151
|
+
return {
|
|
152
|
+
client,
|
|
153
|
+
async close() {
|
|
154
|
+
if (closed) return;
|
|
155
|
+
closed = true;
|
|
156
|
+
await client.close();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function httpTransport(config, env) {
|
|
162
|
+
const url = mcpHttpUrl(config.url);
|
|
163
|
+
const headers = resolveHeaderEnvironment(config, env);
|
|
164
|
+
return new StreamableHTTPClientTransport(url, {
|
|
165
|
+
requestInit: Object.keys(headers).length ? { headers } : undefined
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stdioTransport(config, env) {
|
|
170
|
+
const command = requiredString(config.command, 'MCP stdio command');
|
|
171
|
+
const args = stringArray(config.args);
|
|
172
|
+
const mappedEnvironment = Object.create(null);
|
|
173
|
+
for (const [childName, sourceName] of Object.entries(
|
|
174
|
+
plainObject(config.envMapping) ?? {}
|
|
175
|
+
)) {
|
|
176
|
+
assertEnvironmentName(childName);
|
|
177
|
+
assertEnvironmentName(sourceName);
|
|
178
|
+
mappedEnvironment[childName] = requiredEnvironment(env, sourceName);
|
|
179
|
+
}
|
|
180
|
+
return new StdioClientTransport({
|
|
181
|
+
command,
|
|
182
|
+
args,
|
|
183
|
+
env: { ...getDefaultEnvironment(), ...mappedEnvironment },
|
|
184
|
+
...(config.cwd ? { cwd: requiredString(config.cwd, 'MCP stdio cwd') } : {}),
|
|
185
|
+
stderr: 'pipe'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveHeaderEnvironment(config, env) {
|
|
190
|
+
const headers = Object.create(null);
|
|
191
|
+
if (config.tokenEnv) {
|
|
192
|
+
assertEnvironmentName(config.tokenEnv);
|
|
193
|
+
headers.Authorization = `Bearer ${requiredEnvironment(env, config.tokenEnv)}`;
|
|
194
|
+
}
|
|
195
|
+
for (const [header, sourceName] of Object.entries(
|
|
196
|
+
plainObject(config.headerEnv) ?? {}
|
|
197
|
+
)) {
|
|
198
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u.test(header)) {
|
|
199
|
+
throw new TypeError(`Invalid MCP header name: ${header}`);
|
|
200
|
+
}
|
|
201
|
+
assertEnvironmentName(sourceName);
|
|
202
|
+
headers[header] = requiredEnvironment(env, sourceName);
|
|
203
|
+
}
|
|
204
|
+
return headers;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function withSession(openSession, context, operation) {
|
|
208
|
+
const session = await openSession(context);
|
|
209
|
+
try {
|
|
210
|
+
return await operation(session.client);
|
|
211
|
+
} finally {
|
|
212
|
+
await session.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function filterResources(resources, source = {}) {
|
|
217
|
+
const prefix = source?.resourceUriPrefix;
|
|
218
|
+
return prefix
|
|
219
|
+
? resources.filter(({ uri }) => typeof uri === 'string' && uri.startsWith(prefix))
|
|
220
|
+
: resources;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function resourcePage(client, context) {
|
|
224
|
+
const state = resourcePageCursor(context.cursor);
|
|
225
|
+
const listed = await client.listResources(
|
|
226
|
+
state.serverCursor ? { cursor: state.serverCursor } : {}
|
|
227
|
+
);
|
|
228
|
+
const resources = filterResources(listed.resources ?? [], context.source);
|
|
229
|
+
const limit = connectorLimit(context.limit);
|
|
230
|
+
const page = resources.slice(state.offset, state.offset + limit);
|
|
231
|
+
const nextOffset = state.offset + page.length;
|
|
232
|
+
const nextCursor = nextOffset < resources.length
|
|
233
|
+
? encodeLocalCursor({
|
|
234
|
+
mode: 'resource-page',
|
|
235
|
+
serverCursor: state.serverCursor,
|
|
236
|
+
offset: nextOffset
|
|
237
|
+
})
|
|
238
|
+
: listed.nextCursor ?? null;
|
|
239
|
+
return { resources: page, nextCursor };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resourcePageCursor(value) {
|
|
243
|
+
if (!value) return { serverCursor: null, offset: 0 };
|
|
244
|
+
const local = decodeLocalCursor(value);
|
|
245
|
+
if (!local) return { serverCursor: value, offset: 0 };
|
|
246
|
+
if (local.mode !== 'resource-page' ||
|
|
247
|
+
(local.serverCursor !== null && typeof local.serverCursor !== 'string') ||
|
|
248
|
+
!Number.isInteger(local.offset) || local.offset < 0) {
|
|
249
|
+
throw new TypeError('Invalid MCP resource cursor');
|
|
250
|
+
}
|
|
251
|
+
return { serverCursor: local.serverCursor, offset: local.offset };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function explicitResourceOffset(value) {
|
|
255
|
+
if (!value) return 0;
|
|
256
|
+
const local = decodeLocalCursor(value);
|
|
257
|
+
if (!local || local.mode !== 'explicit' ||
|
|
258
|
+
!Number.isInteger(local.offset) || local.offset < 0) {
|
|
259
|
+
throw new TypeError('Invalid MCP explicit-resource cursor');
|
|
260
|
+
}
|
|
261
|
+
return local.offset;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function encodeLocalCursor(value) {
|
|
265
|
+
return `${LOCAL_CURSOR_PREFIX}${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function decodeLocalCursor(value) {
|
|
269
|
+
if (typeof value !== 'string' || !value.startsWith(LOCAL_CURSOR_PREFIX)) return null;
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(
|
|
272
|
+
Buffer.from(value.slice(LOCAL_CURSOR_PREFIX.length), 'base64url').toString('utf8')
|
|
273
|
+
);
|
|
274
|
+
} catch {
|
|
275
|
+
throw new TypeError('Invalid MCP local cursor');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function connectorLimit(value) {
|
|
280
|
+
if (value === undefined || value === null) return 100;
|
|
281
|
+
if (!Number.isInteger(value) || value <= 0 || value > 100) {
|
|
282
|
+
throw new TypeError('MCP limit must be between 1 and 100');
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resourceText(contents) {
|
|
288
|
+
return contents.map((item) => {
|
|
289
|
+
if (typeof item.text === 'string') return item.text;
|
|
290
|
+
if (typeof item.blob === 'string' && isTextMime(item.mimeType)) {
|
|
291
|
+
return Buffer.from(item.blob, 'base64').toString('utf8');
|
|
292
|
+
}
|
|
293
|
+
return '';
|
|
294
|
+
}).filter(Boolean).join('\n\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function firstMimeType(contents = []) {
|
|
298
|
+
return contents.find(({ mimeType }) => mimeType)?.mimeType ?? null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isTextMime(mimeType) {
|
|
302
|
+
return typeof mimeType === 'string' && (
|
|
303
|
+
mimeType.startsWith('text/') ||
|
|
304
|
+
mimeType.includes('json') ||
|
|
305
|
+
mimeType.includes('xml') ||
|
|
306
|
+
mimeType.includes('yaml')
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function assertReadTool(tools, name) {
|
|
311
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
312
|
+
if (!tool) throw new TypeError(`MCP read tool is unavailable: ${name}`);
|
|
313
|
+
if (tool.annotations?.readOnlyHint === false || tool.annotations?.destructiveHint === true) {
|
|
314
|
+
throw new TypeError(`MCP tool is not declared read-only: ${name}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toolItems(result) {
|
|
319
|
+
const payload = toolPayload(result);
|
|
320
|
+
if (Array.isArray(payload)) return payload;
|
|
321
|
+
if (!payload || typeof payload !== 'object') {
|
|
322
|
+
return typeof payload === 'string' && payload.trim() ? [{ content: payload }] : [];
|
|
323
|
+
}
|
|
324
|
+
for (const key of ['items', 'results', 'hits', 'data']) {
|
|
325
|
+
if (Array.isArray(payload[key])) return payload[key];
|
|
326
|
+
}
|
|
327
|
+
return [payload];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toolPayload(result) {
|
|
331
|
+
if (result?.structuredContent !== undefined) return result.structuredContent;
|
|
332
|
+
const text = (result?.content ?? [])
|
|
333
|
+
.filter((item) => item.type === 'text' && typeof item.text === 'string')
|
|
334
|
+
.map(({ text }) => text)
|
|
335
|
+
.join('\n');
|
|
336
|
+
if (!text) return null;
|
|
337
|
+
try {
|
|
338
|
+
return JSON.parse(text);
|
|
339
|
+
} catch {
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeToolItem(value, query, index) {
|
|
345
|
+
if (typeof value === 'string') {
|
|
346
|
+
return {
|
|
347
|
+
id: `mcp:${index + 1}`,
|
|
348
|
+
title: query,
|
|
349
|
+
content: value,
|
|
350
|
+
url: null,
|
|
351
|
+
updatedAt: null,
|
|
352
|
+
metadata: {}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (!value || typeof value !== 'object') return null;
|
|
356
|
+
const content = itemContent(value);
|
|
357
|
+
if (!content) return null;
|
|
358
|
+
const id = String(value.id ?? value.uri ?? value.url ?? `mcp:${index + 1}`);
|
|
359
|
+
return {
|
|
360
|
+
id,
|
|
361
|
+
title: String(value.title ?? value.name ?? id),
|
|
362
|
+
content,
|
|
363
|
+
url: onlineUrl(value.url ?? value.uri),
|
|
364
|
+
updatedAt: dateTimeOrNull(value.updatedAt ?? value.updated_at),
|
|
365
|
+
metadata: plainObject(value.metadata) ?? {}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function itemContent(value) {
|
|
370
|
+
if (!value || typeof value !== 'object') return '';
|
|
371
|
+
for (const key of ['content', 'text', 'snippet', 'description', 'markdown']) {
|
|
372
|
+
if (typeof value[key] === 'string' && value[key].trim()) return value[key];
|
|
373
|
+
}
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function mergeToolItem(left, right) {
|
|
378
|
+
if (!left || typeof left !== 'object') return right;
|
|
379
|
+
if (!right || typeof right !== 'object') return left;
|
|
380
|
+
return { ...left, ...right };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function mcpHttpUrl(value) {
|
|
384
|
+
const url = new URL(requiredString(value, 'MCP URL'));
|
|
385
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname);
|
|
386
|
+
if (url.username || url.password || (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))) {
|
|
387
|
+
throw new TypeError('MCP URL must use HTTPS, or HTTP on loopback, and cannot contain credentials');
|
|
388
|
+
}
|
|
389
|
+
return url;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function onlineUrl(value) {
|
|
393
|
+
if (typeof value !== 'string') return null;
|
|
394
|
+
try {
|
|
395
|
+
const url = new URL(value);
|
|
396
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
|
|
397
|
+
? url.toString()
|
|
398
|
+
: null;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function dateTimeOrNull(value) {
|
|
405
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) return null;
|
|
406
|
+
return new Date(value).toISOString();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function plainObject(value) {
|
|
410
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function stringArray(value) {
|
|
414
|
+
if (value === undefined || value === null) return [];
|
|
415
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
416
|
+
throw new TypeError('Expected an array of strings');
|
|
417
|
+
}
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function requiredString(value, label) {
|
|
422
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
|
|
423
|
+
return value.trim();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function assertEnvironmentName(value) {
|
|
427
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) {
|
|
428
|
+
throw new TypeError('Environment variable name is invalid');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function requiredEnvironment(env, name) {
|
|
433
|
+
const value = env[name];
|
|
434
|
+
if (typeof value !== 'string' || !value) {
|
|
435
|
+
throw new TypeError(`Required environment variable is unavailable: ${name}`);
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|