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.
Files changed (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -5
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. 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
+ }