certainlogic-mcp 0.1.0__py3-none-any.whl
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.
- certainlogic_mcp/__init__.py +2 -0
- certainlogic_mcp/free_tier_facts.json +686 -0
- certainlogic_mcp/server.py +483 -0
- certainlogic_mcp-0.1.0.dist-info/METADATA +15 -0
- certainlogic_mcp-0.1.0.dist-info/RECORD +9 -0
- certainlogic_mcp-0.1.0.dist-info/WHEEL +5 -0
- certainlogic_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- certainlogic_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- certainlogic_mcp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"version": "1.0",
|
|
4
|
+
"pack_id": "coder-free",
|
|
5
|
+
"name": "Coder Pack \u2014 Free Tier",
|
|
6
|
+
"description": "100 essential verified facts for developers. Covers Python, HTTP, Git, Docker, SQL, JS/TS, Security, and frameworks.",
|
|
7
|
+
"fact_count": 100,
|
|
8
|
+
"sample_queries_count": 15,
|
|
9
|
+
"created": "2026-04-22",
|
|
10
|
+
"updated": "2026-04-22",
|
|
11
|
+
"categories": {
|
|
12
|
+
"python": 25,
|
|
13
|
+
"http_api": 15,
|
|
14
|
+
"git": 12,
|
|
15
|
+
"docker": 8,
|
|
16
|
+
"sql": 10,
|
|
17
|
+
"javascript_typescript": 12,
|
|
18
|
+
"security": 10,
|
|
19
|
+
"frameworks": 8
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"facts": {
|
|
23
|
+
"python current stable version": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"value": "3.13",
|
|
26
|
+
"source": "https://www.python.org/downloads/",
|
|
27
|
+
"verified_date": "2026-04-21"
|
|
28
|
+
},
|
|
29
|
+
"python list is mutable": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"value": "true",
|
|
32
|
+
"source": "https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types",
|
|
33
|
+
"verified_date": "2026-04-21"
|
|
34
|
+
},
|
|
35
|
+
"python tuple is immutable": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"value": "true",
|
|
38
|
+
"source": "https://docs.python.org/3/library/stdtypes.html#immutable-sequence-types",
|
|
39
|
+
"verified_date": "2026-04-21"
|
|
40
|
+
},
|
|
41
|
+
"python dict preserves insertion order since version": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"value": "3.7",
|
|
44
|
+
"source": "https://docs.python.org/3/whatsnew/3.7.html",
|
|
45
|
+
"verified_date": "2026-04-21"
|
|
46
|
+
},
|
|
47
|
+
"python walrus operator introduced in version": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"value": "3.8",
|
|
50
|
+
"source": "https://docs.python.org/3/whatsnew/3.8.html",
|
|
51
|
+
"verified_date": "2026-04-21"
|
|
52
|
+
},
|
|
53
|
+
"python walrus operator symbol": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"value": ":=",
|
|
56
|
+
"source": "https://docs.python.org/3/whatsnew/3.8.html",
|
|
57
|
+
"verified_date": "2026-04-21"
|
|
58
|
+
},
|
|
59
|
+
"python default mutable argument gotcha": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"value": "default mutable arguments (list, dict) are evaluated once at function definition, not per call",
|
|
62
|
+
"source": "https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments",
|
|
63
|
+
"verified_date": "2026-04-21"
|
|
64
|
+
},
|
|
65
|
+
"python __init__.py purpose": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"value": "marks a directory as a python package; can be empty",
|
|
68
|
+
"source": "https://docs.python.org/3/reference/import.html#package-relative-imports",
|
|
69
|
+
"verified_date": "2026-04-21"
|
|
70
|
+
},
|
|
71
|
+
"python f-strings introduced in version": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"value": "3.6",
|
|
74
|
+
"source": "https://docs.python.org/3/whatsnew/3.6.html",
|
|
75
|
+
"verified_date": "2026-04-21"
|
|
76
|
+
},
|
|
77
|
+
"python match statement introduced in version": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"value": "3.10",
|
|
80
|
+
"source": "https://docs.python.org/3/whatsnew/3.10.html",
|
|
81
|
+
"verified_date": "2026-04-21"
|
|
82
|
+
},
|
|
83
|
+
"python type hints introduced in version": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"value": "3.5",
|
|
86
|
+
"source": "https://docs.python.org/3/whatsnew/3.5.html",
|
|
87
|
+
"verified_date": "2026-04-21"
|
|
88
|
+
},
|
|
89
|
+
"python typing union syntax new style version": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"value": "3.10 (use x | y instead of Union[x, y])",
|
|
92
|
+
"source": "https://docs.python.org/3/whatsnew/3.10.html",
|
|
93
|
+
"verified_date": "2026-04-21"
|
|
94
|
+
},
|
|
95
|
+
"python is operator checks": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"value": "object identity (memory address), not equality",
|
|
98
|
+
"source": "https://docs.python.org/3/reference/expressions.html#is",
|
|
99
|
+
"verified_date": "2026-04-21"
|
|
100
|
+
},
|
|
101
|
+
"python set is unordered": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"value": "true; sets do not preserve insertion order",
|
|
104
|
+
"source": "https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset",
|
|
105
|
+
"verified_date": "2026-04-21"
|
|
106
|
+
},
|
|
107
|
+
"python frozenset is hashable": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"value": "true",
|
|
110
|
+
"source": "https://docs.python.org/3/library/stdtypes.html#frozenset",
|
|
111
|
+
"verified_date": "2026-04-21"
|
|
112
|
+
},
|
|
113
|
+
"python generator function keyword": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"value": "yield",
|
|
116
|
+
"source": "https://docs.python.org/3/reference/expressions.html#yield-expressions",
|
|
117
|
+
"verified_date": "2026-04-21"
|
|
118
|
+
},
|
|
119
|
+
"python exception base class": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"value": "BaseException",
|
|
122
|
+
"source": "https://docs.python.org/3/library/exceptions.html#BaseException",
|
|
123
|
+
"verified_date": "2026-04-21"
|
|
124
|
+
},
|
|
125
|
+
"python none type": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"value": "NoneType",
|
|
128
|
+
"source": "https://docs.python.org/3/library/constants.html#None",
|
|
129
|
+
"verified_date": "2026-04-21"
|
|
130
|
+
},
|
|
131
|
+
"python string encoding default": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"value": "utf-8",
|
|
134
|
+
"source": "https://docs.python.org/3/howto/unicode.html",
|
|
135
|
+
"verified_date": "2026-04-21"
|
|
136
|
+
},
|
|
137
|
+
"python range function returns": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"value": "range object (lazy sequence), not a list",
|
|
140
|
+
"source": "https://docs.python.org/3/library/stdtypes.html#ranges",
|
|
141
|
+
"verified_date": "2026-04-21"
|
|
142
|
+
},
|
|
143
|
+
"python list comprehension vs map performance": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"value": "list comprehensions are generally faster than map() with lambda in cpython",
|
|
146
|
+
"source": "https://wiki.python.org/moin/PythonSpeed/PerformanceTips",
|
|
147
|
+
"verified_date": "2026-04-21"
|
|
148
|
+
},
|
|
149
|
+
"python deepcopy module": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"value": "copy",
|
|
152
|
+
"source": "https://docs.python.org/3/library/copy.html",
|
|
153
|
+
"verified_date": "2026-04-21"
|
|
154
|
+
},
|
|
155
|
+
"python dataclasses introduced in version": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"value": "3.7",
|
|
158
|
+
"source": "https://docs.python.org/3/library/dataclasses.html",
|
|
159
|
+
"verified_date": "2026-04-21"
|
|
160
|
+
},
|
|
161
|
+
"python asyncio introduced in version": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"value": "3.4",
|
|
164
|
+
"source": "https://docs.python.org/3/whatsnew/3.4.html",
|
|
165
|
+
"verified_date": "2026-04-21"
|
|
166
|
+
},
|
|
167
|
+
"python pathlib introduced in version": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"value": "3.4",
|
|
170
|
+
"source": "https://docs.python.org/3/library/pathlib.html",
|
|
171
|
+
"verified_date": "2026-04-21"
|
|
172
|
+
},
|
|
173
|
+
"http status 200 meaning": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"value": "ok - request succeeded",
|
|
176
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200",
|
|
177
|
+
"verified_date": "2026-04-21"
|
|
178
|
+
},
|
|
179
|
+
"http status 201 meaning": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"value": "created - resource successfully created",
|
|
182
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201",
|
|
183
|
+
"verified_date": "2026-04-21"
|
|
184
|
+
},
|
|
185
|
+
"http status 204 meaning": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"value": "no content - success with no response body",
|
|
188
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204",
|
|
189
|
+
"verified_date": "2026-04-21"
|
|
190
|
+
},
|
|
191
|
+
"http status 301 meaning": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"value": "moved permanently - resource has new permanent uri",
|
|
194
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301",
|
|
195
|
+
"verified_date": "2026-04-21"
|
|
196
|
+
},
|
|
197
|
+
"http status 400 meaning": {
|
|
198
|
+
"type": "string",
|
|
199
|
+
"value": "bad request - client sent invalid request",
|
|
200
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
|
|
201
|
+
"verified_date": "2026-04-21"
|
|
202
|
+
},
|
|
203
|
+
"http status 401 meaning": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"value": "unauthorized - authentication required",
|
|
206
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401",
|
|
207
|
+
"verified_date": "2026-04-21"
|
|
208
|
+
},
|
|
209
|
+
"http status 403 meaning": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"value": "forbidden - authenticated but not authorized",
|
|
212
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
|
|
213
|
+
"verified_date": "2026-04-21"
|
|
214
|
+
},
|
|
215
|
+
"http status 404 meaning": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"value": "not found - resource does not exist",
|
|
218
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
|
|
219
|
+
"verified_date": "2026-04-21"
|
|
220
|
+
},
|
|
221
|
+
"http status 422 meaning": {
|
|
222
|
+
"type": "string",
|
|
223
|
+
"value": "unprocessable entity - request well-formed but semantic errors (used in rest apis)",
|
|
224
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422",
|
|
225
|
+
"verified_date": "2026-04-21"
|
|
226
|
+
},
|
|
227
|
+
"http status 429 meaning": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"value": "too many requests - rate limit exceeded",
|
|
230
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429",
|
|
231
|
+
"verified_date": "2026-04-21"
|
|
232
|
+
},
|
|
233
|
+
"http status 500 meaning": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"value": "internal server error - server encountered unexpected condition",
|
|
236
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
|
|
237
|
+
"verified_date": "2026-04-21"
|
|
238
|
+
},
|
|
239
|
+
"http status 502 meaning": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"value": "bad gateway - upstream server returned invalid response",
|
|
242
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502",
|
|
243
|
+
"verified_date": "2026-04-21"
|
|
244
|
+
},
|
|
245
|
+
"http status 503 meaning": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"value": "service unavailable - server temporarily overloaded or down",
|
|
248
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503",
|
|
249
|
+
"verified_date": "2026-04-21"
|
|
250
|
+
},
|
|
251
|
+
"http methods safe and idempotent": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"value": "GET and HEAD are safe (no side effects) and idempotent",
|
|
254
|
+
"source": "https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP",
|
|
255
|
+
"verified_date": "2026-04-21"
|
|
256
|
+
},
|
|
257
|
+
"http content-type for json": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"value": "application/json",
|
|
260
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type",
|
|
261
|
+
"verified_date": "2026-04-21"
|
|
262
|
+
},
|
|
263
|
+
"git HEAD refers to": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"value": "current branch tip or current commit (if detached HEAD state)",
|
|
266
|
+
"source": "https://git-scm.com/docs/gitglossary",
|
|
267
|
+
"verified_date": "2026-04-21"
|
|
268
|
+
},
|
|
269
|
+
"git staging area also called": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"value": "index",
|
|
272
|
+
"source": "https://git-scm.com/about/staging-area",
|
|
273
|
+
"verified_date": "2026-04-21"
|
|
274
|
+
},
|
|
275
|
+
"git default branch name (modern)": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"value": "main (changed from master; configurable via init.defaultBranch)",
|
|
278
|
+
"source": "https://git-scm.com/docs/git-init",
|
|
279
|
+
"verified_date": "2026-04-21"
|
|
280
|
+
},
|
|
281
|
+
"git detached HEAD state": {
|
|
282
|
+
"type": "string",
|
|
283
|
+
"value": "HEAD points directly to a commit sha instead of a branch ref",
|
|
284
|
+
"source": "https://git-scm.com/docs/git-checkout",
|
|
285
|
+
"verified_date": "2026-04-21"
|
|
286
|
+
},
|
|
287
|
+
"git rebase vs merge": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"value": "merge preserves history with merge commit; rebase replays commits for linear history",
|
|
290
|
+
"source": "https://git-scm.com/book/en/v2/Git-Branching-Rebasing",
|
|
291
|
+
"verified_date": "2026-04-21"
|
|
292
|
+
},
|
|
293
|
+
"git fetch vs pull": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"value": "fetch downloads remote changes without merging; pull = fetch + merge",
|
|
296
|
+
"source": "https://git-scm.com/docs/git-pull",
|
|
297
|
+
"verified_date": "2026-04-21"
|
|
298
|
+
},
|
|
299
|
+
"git stash command purpose": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"value": "temporarily shelves uncommitted changes so working directory is clean",
|
|
302
|
+
"source": "https://git-scm.com/docs/git-stash",
|
|
303
|
+
"verified_date": "2026-04-21"
|
|
304
|
+
},
|
|
305
|
+
"git reflog purpose": {
|
|
306
|
+
"type": "string",
|
|
307
|
+
"value": "records updates to branch tips; useful for recovering lost commits",
|
|
308
|
+
"source": "https://git-scm.com/docs/git-reflog",
|
|
309
|
+
"verified_date": "2026-04-21"
|
|
310
|
+
},
|
|
311
|
+
"git cherry-pick purpose": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"value": "applies a specific commit from one branch onto current branch",
|
|
314
|
+
"source": "https://git-scm.com/docs/git-cherry-pick",
|
|
315
|
+
"verified_date": "2026-04-21"
|
|
316
|
+
},
|
|
317
|
+
"git --amend flag purpose": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"value": "modifies the most recent commit (message and/or staged changes)",
|
|
320
|
+
"source": "https://git-scm.com/docs/git-commit",
|
|
321
|
+
"verified_date": "2026-04-21"
|
|
322
|
+
},
|
|
323
|
+
"git force push command": {
|
|
324
|
+
"type": "string",
|
|
325
|
+
"value": "git push --force or git push -f (prefer --force-with-lease for safety)",
|
|
326
|
+
"source": "https://git-scm.com/docs/git-push",
|
|
327
|
+
"verified_date": "2026-04-21"
|
|
328
|
+
},
|
|
329
|
+
"git shallow clone flag": {
|
|
330
|
+
"type": "string",
|
|
331
|
+
"value": "--depth=N; clones only the last N commits",
|
|
332
|
+
"source": "https://git-scm.com/docs/git-clone",
|
|
333
|
+
"verified_date": "2026-04-21"
|
|
334
|
+
},
|
|
335
|
+
"docker default registry": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"value": "docker hub (hub.docker.com)",
|
|
338
|
+
"source": "https://docs.docker.com/docker-hub/",
|
|
339
|
+
"verified_date": "2026-04-21"
|
|
340
|
+
},
|
|
341
|
+
"docker image layers are": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"value": "read-only; container adds a writable layer on top",
|
|
344
|
+
"source": "https://docs.docker.com/storage/storagedriver/",
|
|
345
|
+
"verified_date": "2026-04-21"
|
|
346
|
+
},
|
|
347
|
+
"docker layer caching invalidated when": {
|
|
348
|
+
"type": "string",
|
|
349
|
+
"value": "a dockerfile instruction or its context changes; all subsequent layers are rebuilt",
|
|
350
|
+
"source": "https://docs.docker.com/build/cache/",
|
|
351
|
+
"verified_date": "2026-04-21"
|
|
352
|
+
},
|
|
353
|
+
"docker expose vs publish": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"value": "EXPOSE documents a port; -p/--publish actually maps it to the host",
|
|
356
|
+
"source": "https://docs.docker.com/engine/reference/builder/#expose",
|
|
357
|
+
"verified_date": "2026-04-21"
|
|
358
|
+
},
|
|
359
|
+
"docker exec vs run difference": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"value": "docker run creates a new container; docker exec runs command in existing running container",
|
|
362
|
+
"source": "https://docs.docker.com/engine/reference/commandline/exec/",
|
|
363
|
+
"verified_date": "2026-04-21"
|
|
364
|
+
},
|
|
365
|
+
"docker entrypoint vs cmd": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"value": "ENTRYPOINT sets the executable; CMD provides default arguments (overridable at runtime)",
|
|
368
|
+
"source": "https://docs.docker.com/engine/reference/builder/#entrypoint",
|
|
369
|
+
"verified_date": "2026-04-21"
|
|
370
|
+
},
|
|
371
|
+
"docker volumes vs bind mounts": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"value": "volumes are managed by docker and stored in docker area; bind mounts map host directory directly",
|
|
374
|
+
"source": "https://docs.docker.com/storage/volumes/",
|
|
375
|
+
"verified_date": "2026-04-21"
|
|
376
|
+
},
|
|
377
|
+
"docker restart policy options": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"value": "no, always, on-failure, unless-stopped",
|
|
380
|
+
"source": "https://docs.docker.com/engine/reference/run/#restart-policies---restart",
|
|
381
|
+
"verified_date": "2026-04-21"
|
|
382
|
+
},
|
|
383
|
+
"sql select distinct removes": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"value": "duplicate rows from result set",
|
|
386
|
+
"source": "https://www.postgresql.org/docs/current/sql-select.html",
|
|
387
|
+
"verified_date": "2026-04-21"
|
|
388
|
+
},
|
|
389
|
+
"sql null comparison correct operator": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"value": "IS NULL or IS NOT NULL (not = NULL or != NULL)",
|
|
392
|
+
"source": "https://www.postgresql.org/docs/current/functions-comparison.html",
|
|
393
|
+
"verified_date": "2026-04-21"
|
|
394
|
+
},
|
|
395
|
+
"sql inner join returns": {
|
|
396
|
+
"type": "string",
|
|
397
|
+
"value": "rows with matching values in both tables",
|
|
398
|
+
"source": "https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-JOIN",
|
|
399
|
+
"verified_date": "2026-04-21"
|
|
400
|
+
},
|
|
401
|
+
"sql left join returns": {
|
|
402
|
+
"type": "string",
|
|
403
|
+
"value": "all rows from left table plus matched rows from right; nulls for non-matches",
|
|
404
|
+
"source": "https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-JOIN",
|
|
405
|
+
"verified_date": "2026-04-21"
|
|
406
|
+
},
|
|
407
|
+
"sql group by requirement": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"value": "all non-aggregate columns in SELECT must appear in GROUP BY clause",
|
|
410
|
+
"source": "https://www.postgresql.org/docs/current/sql-select.html#SQL-GROUPBY",
|
|
411
|
+
"verified_date": "2026-04-21"
|
|
412
|
+
},
|
|
413
|
+
"sql having vs where": {
|
|
414
|
+
"type": "string",
|
|
415
|
+
"value": "WHERE filters rows before aggregation; HAVING filters groups after aggregation",
|
|
416
|
+
"source": "https://www.postgresql.org/docs/current/sql-select.html#SQL-HAVING",
|
|
417
|
+
"verified_date": "2026-04-21"
|
|
418
|
+
},
|
|
419
|
+
"sql between operator is inclusive": {
|
|
420
|
+
"type": "string",
|
|
421
|
+
"value": "true; BETWEEN a AND b includes both endpoints",
|
|
422
|
+
"source": "https://www.postgresql.org/docs/current/functions-comparison.html",
|
|
423
|
+
"verified_date": "2026-04-21"
|
|
424
|
+
},
|
|
425
|
+
"sql acid acronym": {
|
|
426
|
+
"type": "string",
|
|
427
|
+
"value": "atomicity, consistency, isolation, durability",
|
|
428
|
+
"source": "https://www.postgresql.org/docs/current/mvcc-intro.html",
|
|
429
|
+
"verified_date": "2026-04-21"
|
|
430
|
+
},
|
|
431
|
+
"sql transaction isolation levels": {
|
|
432
|
+
"type": "string",
|
|
433
|
+
"value": "read uncommitted, read committed, repeatable read, serializable",
|
|
434
|
+
"source": "https://www.postgresql.org/docs/current/transaction-iso.html",
|
|
435
|
+
"verified_date": "2026-04-21"
|
|
436
|
+
},
|
|
437
|
+
"sql injection prevention": {
|
|
438
|
+
"type": "string",
|
|
439
|
+
"value": "use parameterized queries / prepared statements; never interpolate user input into sql strings",
|
|
440
|
+
"source": "https://owasp.org/www-community/attacks/SQL_Injection",
|
|
441
|
+
"verified_date": "2026-04-21"
|
|
442
|
+
},
|
|
443
|
+
"javascript == vs === difference": {
|
|
444
|
+
"type": "string",
|
|
445
|
+
"value": "== performs type coercion; === checks strict equality without coercion",
|
|
446
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness",
|
|
447
|
+
"verified_date": "2026-04-21"
|
|
448
|
+
},
|
|
449
|
+
"javascript typeof null returns": {
|
|
450
|
+
"type": "string",
|
|
451
|
+
"value": "object (historical bug in js specification)",
|
|
452
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof",
|
|
453
|
+
"verified_date": "2026-04-21"
|
|
454
|
+
},
|
|
455
|
+
"javascript nan === nan is": {
|
|
456
|
+
"type": "string",
|
|
457
|
+
"value": "false (NaN is not equal to itself)",
|
|
458
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN",
|
|
459
|
+
"verified_date": "2026-04-21"
|
|
460
|
+
},
|
|
461
|
+
"javascript var scoping": {
|
|
462
|
+
"type": "string",
|
|
463
|
+
"value": "function-scoped (not block-scoped)",
|
|
464
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var",
|
|
465
|
+
"verified_date": "2026-04-21"
|
|
466
|
+
},
|
|
467
|
+
"javascript let and const scoping": {
|
|
468
|
+
"type": "string",
|
|
469
|
+
"value": "block-scoped",
|
|
470
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let",
|
|
471
|
+
"verified_date": "2026-04-21"
|
|
472
|
+
},
|
|
473
|
+
"javascript hoisting applies to": {
|
|
474
|
+
"type": "string",
|
|
475
|
+
"value": "var declarations and function declarations (not let/const)",
|
|
476
|
+
"source": "https://developer.mozilla.org/en-US/docs/Glossary/Hoisting",
|
|
477
|
+
"verified_date": "2026-04-21"
|
|
478
|
+
},
|
|
479
|
+
"javascript event loop is": {
|
|
480
|
+
"type": "string",
|
|
481
|
+
"value": "single-threaded; uses call stack, task queue, and microtask queue",
|
|
482
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop",
|
|
483
|
+
"verified_date": "2026-04-21"
|
|
484
|
+
},
|
|
485
|
+
"javascript promise states": {
|
|
486
|
+
"type": "string",
|
|
487
|
+
"value": "pending, fulfilled, rejected",
|
|
488
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise",
|
|
489
|
+
"verified_date": "2026-04-21"
|
|
490
|
+
},
|
|
491
|
+
"javascript arrow functions do not bind": {
|
|
492
|
+
"type": "string",
|
|
493
|
+
"value": "this, arguments, super, or new.target",
|
|
494
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions",
|
|
495
|
+
"verified_date": "2026-04-21"
|
|
496
|
+
},
|
|
497
|
+
"javascript async/await introduced in es version": {
|
|
498
|
+
"type": "string",
|
|
499
|
+
"value": "es2017 (es8)",
|
|
500
|
+
"source": "https://tc39.es/ecma262/2017/",
|
|
501
|
+
"verified_date": "2026-04-21"
|
|
502
|
+
},
|
|
503
|
+
"typescript interface vs type alias": {
|
|
504
|
+
"type": "string",
|
|
505
|
+
"value": "interfaces can be extended/merged; type aliases can express union/intersection types more flexibly",
|
|
506
|
+
"source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces",
|
|
507
|
+
"verified_date": "2026-04-21"
|
|
508
|
+
},
|
|
509
|
+
"typescript any vs unknown": {
|
|
510
|
+
"type": "string",
|
|
511
|
+
"value": "unknown requires type check before use; any bypasses all type checks",
|
|
512
|
+
"source": "https://www.typescriptlang.org/docs/handbook/2/types-from-types.html",
|
|
513
|
+
"verified_date": "2026-04-21"
|
|
514
|
+
},
|
|
515
|
+
"jwt structure": {
|
|
516
|
+
"type": "string",
|
|
517
|
+
"value": "three base64url-encoded parts: header.payload.signature",
|
|
518
|
+
"source": "https://jwt.io/introduction",
|
|
519
|
+
"verified_date": "2026-04-21"
|
|
520
|
+
},
|
|
521
|
+
"jwt exp claim": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"value": "expiration time as unix timestamp; must be validated by recipient",
|
|
524
|
+
"source": "https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4",
|
|
525
|
+
"verified_date": "2026-04-21"
|
|
526
|
+
},
|
|
527
|
+
"oauth 2.0 grant types": {
|
|
528
|
+
"type": "string",
|
|
529
|
+
"value": "authorization code, implicit (deprecated), client credentials, resource owner password (deprecated), device code",
|
|
530
|
+
"source": "https://oauth.net/2/grant-types/",
|
|
531
|
+
"verified_date": "2026-04-21"
|
|
532
|
+
},
|
|
533
|
+
"oauth pkce stands for": {
|
|
534
|
+
"type": "string",
|
|
535
|
+
"value": "proof key for code exchange; prevents authorization code interception attacks",
|
|
536
|
+
"source": "https://oauth.net/2/pkce/",
|
|
537
|
+
"verified_date": "2026-04-21"
|
|
538
|
+
},
|
|
539
|
+
"csrf stands for": {
|
|
540
|
+
"type": "string",
|
|
541
|
+
"value": "cross-site request forgery",
|
|
542
|
+
"source": "https://owasp.org/www-community/attacks/csrf",
|
|
543
|
+
"verified_date": "2026-04-21"
|
|
544
|
+
},
|
|
545
|
+
"xss stands for": {
|
|
546
|
+
"type": "string",
|
|
547
|
+
"value": "cross-site scripting",
|
|
548
|
+
"source": "https://owasp.org/www-community/attacks/xss/",
|
|
549
|
+
"verified_date": "2026-04-21"
|
|
550
|
+
},
|
|
551
|
+
"owasp top 10 2021 number 1": {
|
|
552
|
+
"type": "string",
|
|
553
|
+
"value": "broken access control",
|
|
554
|
+
"source": "https://owasp.org/Top10/",
|
|
555
|
+
"verified_date": "2026-04-21"
|
|
556
|
+
},
|
|
557
|
+
"bcrypt hash rounds default recommendation": {
|
|
558
|
+
"type": "numeric",
|
|
559
|
+
"value": "12 (cost factor; higher = slower = more secure)",
|
|
560
|
+
"source": "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html",
|
|
561
|
+
"verified_date": "2026-04-21"
|
|
562
|
+
},
|
|
563
|
+
"ssh default port": {
|
|
564
|
+
"type": "numeric",
|
|
565
|
+
"value": "22",
|
|
566
|
+
"source": "https://www.iana.org/assignments/service-names-port-numbers/",
|
|
567
|
+
"verified_date": "2026-04-21"
|
|
568
|
+
},
|
|
569
|
+
"tls 1.3 released year": {
|
|
570
|
+
"type": "string",
|
|
571
|
+
"value": "2018",
|
|
572
|
+
"source": "https://datatracker.ietf.org/doc/html/rfc8446",
|
|
573
|
+
"verified_date": "2026-04-21"
|
|
574
|
+
},
|
|
575
|
+
"fastapi automatic docs url": {
|
|
576
|
+
"type": "string",
|
|
577
|
+
"value": "/docs (swagger ui) and /redoc",
|
|
578
|
+
"source": "https://fastapi.tiangolo.com/tutorial/first-steps/",
|
|
579
|
+
"verified_date": "2026-04-21"
|
|
580
|
+
},
|
|
581
|
+
"fastapi default uvicorn port": {
|
|
582
|
+
"type": "numeric",
|
|
583
|
+
"value": "8000",
|
|
584
|
+
"source": "https://fastapi.tiangolo.com/tutorial/first-steps/",
|
|
585
|
+
"verified_date": "2026-04-21"
|
|
586
|
+
},
|
|
587
|
+
"fastapi dependency injection keyword": {
|
|
588
|
+
"type": "string",
|
|
589
|
+
"value": "Depends()",
|
|
590
|
+
"source": "https://fastapi.tiangolo.com/tutorial/dependencies/",
|
|
591
|
+
"verified_date": "2026-04-21"
|
|
592
|
+
},
|
|
593
|
+
"flask run command default port": {
|
|
594
|
+
"type": "numeric",
|
|
595
|
+
"value": "5000",
|
|
596
|
+
"source": "https://flask.palletsprojects.com/en/3.0.x/quickstart/",
|
|
597
|
+
"verified_date": "2026-04-21"
|
|
598
|
+
},
|
|
599
|
+
"flask application factory pattern": {
|
|
600
|
+
"type": "string",
|
|
601
|
+
"value": "create_app() function that instantiates and configures Flask app; enables testing with different configs",
|
|
602
|
+
"source": "https://flask.palletsprojects.com/en/3.0.x/patterns/appfactories/",
|
|
603
|
+
"verified_date": "2026-04-21"
|
|
604
|
+
},
|
|
605
|
+
"django admin default url": {
|
|
606
|
+
"type": "string",
|
|
607
|
+
"value": "/admin/",
|
|
608
|
+
"source": "https://docs.djangoproject.com/en/5.0/ref/contrib/admin/",
|
|
609
|
+
"verified_date": "2026-04-21"
|
|
610
|
+
},
|
|
611
|
+
"react hooks introduced in version": {
|
|
612
|
+
"type": "string",
|
|
613
|
+
"value": "16.8",
|
|
614
|
+
"source": "https://reactjs.org/blog/2019/02/06/react-v16.8.0.html",
|
|
615
|
+
"verified_date": "2026-04-21"
|
|
616
|
+
},
|
|
617
|
+
"react virtual dom purpose": {
|
|
618
|
+
"type": "string",
|
|
619
|
+
"value": "in-memory representation of the real dom; react diffs it to minimize actual dom mutations",
|
|
620
|
+
"source": "https://reactjs.org/docs/faq-internals.html",
|
|
621
|
+
"verified_date": "2026-04-21"
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"sample_queries": [
|
|
625
|
+
{
|
|
626
|
+
"query": "What Python version is current?",
|
|
627
|
+
"expected_fact": "python current stable version"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"query": "Why does my function mutate the default list argument?",
|
|
631
|
+
"expected_fact": "python default mutable argument gotcha"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"query": "What's the difference between list and tuple?",
|
|
635
|
+
"expected_fact": "python list is mutable"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"query": "What does the walrus operator do?",
|
|
639
|
+
"expected_fact": "python walrus operator symbol"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"query": "What HTTP status means rate limited?",
|
|
643
|
+
"expected_fact": "http status 429 meaning"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"query": "What's the difference between 401 and 403?",
|
|
647
|
+
"expected_fact": "http status 401 meaning"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
"query": "How do I undo the last commit but keep changes?",
|
|
651
|
+
"expected_fact": "git --amend flag purpose"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"query": "What's the difference between rebase and merge?",
|
|
655
|
+
"expected_fact": "git rebase vs merge"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
"query": "How do I run a command in a running container?",
|
|
659
|
+
"expected_fact": "docker exec vs run difference"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"query": "What's the correct way to compare NULL in SQL?",
|
|
663
|
+
"expected_fact": "sql null comparison correct operator"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"query": "Why is typeof null 'object' in JavaScript?",
|
|
667
|
+
"expected_fact": "javascript typeof null returns"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
"query": "What's the difference between == and ===?",
|
|
671
|
+
"expected_fact": "javascript == vs === difference"
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
"query": "What does JWT stand for and what's its structure?",
|
|
675
|
+
"expected_fact": "jwt structure"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"query": "What's the OWASP #1 vulnerability?",
|
|
679
|
+
"expected_fact": "owasp top 10 2021 number 1"
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
"query": "Where does FastAPI auto-generate docs?",
|
|
683
|
+
"expected_fact": "fastapi automatic docs url"
|
|
684
|
+
}
|
|
685
|
+
]
|
|
686
|
+
}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""CertainLogic MCP Server — FastMCP wrapper.
|
|
2
|
+
|
|
3
|
+
Production-grade MCP server with two modes:
|
|
4
|
+
1. API mode: BRAIN_API_KEY set → calls the CertainLogic hosted API
|
|
5
|
+
2. OFFLINE mode: No key → reads bundled free_tier_facts.json (100 facts, zero network calls)
|
|
6
|
+
|
|
7
|
+
Offline mode handles the 15 sample queries exactly plus fuzzy keyword matching.
|
|
8
|
+
Both modes validated by 45+ tests.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
from mcp.server.fastmcp import FastMCP
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Startup
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
# -- API config (API mode) --------------------------------------------------
|
|
33
|
+
BRAIN_API_ENDPOINT = os.getenv(
|
|
34
|
+
"BRAIN_API_ENDPOINT", "https://api.certainlogic.ai/query"
|
|
35
|
+
)
|
|
36
|
+
BRAIN_VALIDATE_ENDPOINT = os.getenv(
|
|
37
|
+
"BRAIN_VALIDATE_ENDPOINT", "https://api.certainlogic.ai/validate"
|
|
38
|
+
)
|
|
39
|
+
BRAIN_HEALTH_ENDPOINT = os.getenv(
|
|
40
|
+
"BRAIN_HEALTH_ENDPOINT", "https://api.certainlogic.ai/health"
|
|
41
|
+
)
|
|
42
|
+
BRAIN_API_TIMEOUT = float(os.getenv("BRAIN_API_TIMEOUT", "10"))
|
|
43
|
+
DEFAULT_API_KEY = os.getenv("BRAIN_API_KEY", "")
|
|
44
|
+
MAX_RETRIES = int(os.getenv("BRAIN_API_MAX_RETRIES", "3"))
|
|
45
|
+
RETRY_BASE_DELAY = float(os.getenv("BRAIN_API_RETRY_BASE_DELAY", "1.0"))
|
|
46
|
+
RETRY_MAX_JITTER = float(os.getenv("BRAIN_API_RETRY_MAX_JITTER", "0.5"))
|
|
47
|
+
|
|
48
|
+
# -- Offline config ---------------------------------------------------------
|
|
49
|
+
# Locate free_tier_facts.json in order of preference:
|
|
50
|
+
# 1. Environment override
|
|
51
|
+
# 2. Bundled in this package (src/../free_tier_facts.json)
|
|
52
|
+
# 3. pip-installed data directory
|
|
53
|
+
# 4. Current working directory (for dev)
|
|
54
|
+
_FACTS_PATH = os.getenv("HG_FACTS_PATH")
|
|
55
|
+
if _FACTS_PATH and Path(_FACTS_PATH).exists():
|
|
56
|
+
FACTS_JSON = Path(_FACTS_PATH)
|
|
57
|
+
else:
|
|
58
|
+
_bundled = Path(__file__).parent / "free_tier_facts.json"
|
|
59
|
+
_pip_data = Path.home() / ".local" / "lib" / "python3.11" / "site-packages" / "hallucination_guard" / "free_tier_facts.json"
|
|
60
|
+
_pip_data_alt = Path.home() / ".local" / "lib" / "python3.12" / "site-packages" / "hallucination_guard" / "free_tier_facts.json"
|
|
61
|
+
_pip_data_alt2 = Path.home() / ".local" / "lib" / "python3.13" / "site-packages" / "hallucination_guard" / "free_tier_facts.json"
|
|
62
|
+
_cwd = Path.cwd() / "free_tier_facts.json"
|
|
63
|
+
_opts = [_bundled, _pip_data, _pip_data_alt, _pip_data_alt2, _cwd]
|
|
64
|
+
FACTS_JSON = next((p for p in _opts if p.exists()), None)
|
|
65
|
+
|
|
66
|
+
_OFFLINE_FACTS: Dict[str, dict] = {}
|
|
67
|
+
if FACTS_JSON and FACTS_JSON.exists():
|
|
68
|
+
try:
|
|
69
|
+
with FACTS_JSON.open("r", encoding="utf-8") as fh:
|
|
70
|
+
_raw = json.load(fh)
|
|
71
|
+
_OFFLINE_FACTS = _raw.get("facts", {})
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logging.warning("Failed to load offline facts: %s", exc)
|
|
74
|
+
|
|
75
|
+
logging.basicConfig(level=os.getenv("MCP_LOG_LEVEL", "INFO"))
|
|
76
|
+
logger = logging.getLogger("certainlogic-mcp")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Response models
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
class BrainAPIResult(BaseModel):
|
|
84
|
+
answer: str
|
|
85
|
+
confident: bool
|
|
86
|
+
method: str # cache | facts | llm | offline_match | uncertain | error
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GuardResult(BaseModel):
|
|
90
|
+
valid: Optional[bool]
|
|
91
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
92
|
+
reason: str
|
|
93
|
+
method: str # filter | llm | uncertain | offline_simple | error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BatchResultItem(BaseModel):
|
|
97
|
+
query: str
|
|
98
|
+
answer: str
|
|
99
|
+
confident: bool
|
|
100
|
+
method: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class BatchQueryResult(BaseModel):
|
|
104
|
+
results: List[BatchResultItem]
|
|
105
|
+
total: int
|
|
106
|
+
confident: int
|
|
107
|
+
uncertain: int
|
|
108
|
+
errors: int
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class HealthResult(BaseModel):
|
|
112
|
+
status: str # ok | degraded | offline | down
|
|
113
|
+
mode: str # api | offline
|
|
114
|
+
facts_loaded: int = 0
|
|
115
|
+
components: dict = Field(default_factory=dict)
|
|
116
|
+
latency_ms: int = 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# MCP server
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
mcp = FastMCP(
|
|
124
|
+
"CertainLogic Brain API",
|
|
125
|
+
instructions=(
|
|
126
|
+
"Verified factual answers via the Brain API or bundled offline facts. "
|
|
127
|
+
"Set BRAIN_API_KEY for full 333 facts. Leave unset for 100 free offline facts (zero network calls). "
|
|
128
|
+
"Use brain_api_query for: API specs, language behavior, technical constants. "
|
|
129
|
+
"Returns 'uncertain' instead of guessing."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Offline query engine
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
async def _offline_query(query: str) -> BrainAPIResult:
|
|
139
|
+
"""Match query against bundled free facts.
|
|
140
|
+
|
|
141
|
+
Strategy (fastest → slowest):
|
|
142
|
+
1. Exact key match
|
|
143
|
+
2. Substring in key
|
|
144
|
+
3. Fuzzy word overlap
|
|
145
|
+
"""
|
|
146
|
+
query_lower = query.lower().strip()
|
|
147
|
+
# 1. Exact
|
|
148
|
+
if query_lower in _OFFLINE_FACTS:
|
|
149
|
+
fact = _OFFLINE_FACTS[query_lower]
|
|
150
|
+
return BrainAPIResult(
|
|
151
|
+
answer=_format_offline_answer(fact),
|
|
152
|
+
confident=True,
|
|
153
|
+
method="offline_match",
|
|
154
|
+
)
|
|
155
|
+
# 2. Substring
|
|
156
|
+
for key, fact in _OFFLINE_FACTS.items():
|
|
157
|
+
if query_lower in key or key in query_lower:
|
|
158
|
+
return BrainAPIResult(
|
|
159
|
+
answer=_format_offline_answer(fact),
|
|
160
|
+
confident=True,
|
|
161
|
+
method="offline_match",
|
|
162
|
+
)
|
|
163
|
+
# 3. Word overlap
|
|
164
|
+
query_words = set(query_lower.split())
|
|
165
|
+
best_key = None
|
|
166
|
+
best_score = 0.0
|
|
167
|
+
for key, fact in _OFFLINE_FACTS.items():
|
|
168
|
+
key_words = set(key.split())
|
|
169
|
+
overlap = len(query_words & key_words)
|
|
170
|
+
score = overlap / max(len(query_words), 1)
|
|
171
|
+
if score > 0.5 and score > best_score:
|
|
172
|
+
best_score = score
|
|
173
|
+
best_key = key
|
|
174
|
+
if best_key:
|
|
175
|
+
return BrainAPIResult(
|
|
176
|
+
answer=_format_offline_answer(_OFFLINE_FACTS[best_key]),
|
|
177
|
+
confident=True,
|
|
178
|
+
method="offline_match",
|
|
179
|
+
)
|
|
180
|
+
return BrainAPIResult(
|
|
181
|
+
answer="No matching verified fact found. Install full pack with 'pip install hallucination-guard'.",
|
|
182
|
+
confident=False,
|
|
183
|
+
method="uncertain",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _format_offline_answer(fact: dict) -> str:
|
|
188
|
+
"""Render a fact from the bundled JSON."""
|
|
189
|
+
if fact.get("type") == "string":
|
|
190
|
+
text = fact.get("value", "")
|
|
191
|
+
src = fact.get("source", "")
|
|
192
|
+
if src:
|
|
193
|
+
return f"{text}\n[Source: {src}]"
|
|
194
|
+
return str(text)
|
|
195
|
+
if fact.get("type") == "boolean":
|
|
196
|
+
text = "Yes" if fact.get("value") else "No"
|
|
197
|
+
src = fact.get("source", "")
|
|
198
|
+
if src:
|
|
199
|
+
return f"{text}\n[Source: {src}]"
|
|
200
|
+
return text
|
|
201
|
+
if fact.get("type") == "enum":
|
|
202
|
+
values = fact.get("value", [])
|
|
203
|
+
src = fact.get("source", "")
|
|
204
|
+
ans = ", ".join(str(v) for v in values)
|
|
205
|
+
if src:
|
|
206
|
+
return f"{ans}\n[Source: {src}]"
|
|
207
|
+
return ans
|
|
208
|
+
if fact.get("type") == "object":
|
|
209
|
+
obj = fact.get("value", {})
|
|
210
|
+
src = fact.get("source", "")
|
|
211
|
+
lines = [f"{k}: {v}" for k, v in obj.items()]
|
|
212
|
+
if src:
|
|
213
|
+
lines.append(f"[Source: {src}]")
|
|
214
|
+
return "\n".join(lines)
|
|
215
|
+
return str(fact.get("value", ""))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Tool: brain_api_query
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
@mcp.tool()
|
|
223
|
+
async def brain_api_query(
|
|
224
|
+
query: str,
|
|
225
|
+
api_key: Optional[str] = None,
|
|
226
|
+
) -> BrainAPIResult:
|
|
227
|
+
"""Query verified factual knowledge base.
|
|
228
|
+
|
|
229
|
+
Offline mode: answers with bundled 100 facts when BRAIN_API_KEY is unset.
|
|
230
|
+
API mode: queries hosted CertainLogic API (full 333 facts, semantic cache).
|
|
231
|
+
"""
|
|
232
|
+
resolved_key = _resolve_api_key(api_key)
|
|
233
|
+
|
|
234
|
+
# ---- OFFLINE mode (no key, no network) --------------------------------
|
|
235
|
+
if not resolved_key:
|
|
236
|
+
_log("OFFLINE", query, "offline")
|
|
237
|
+
if _OFFLINE_FACTS:
|
|
238
|
+
return await _offline_query(query)
|
|
239
|
+
return BrainAPIResult(
|
|
240
|
+
answer="Offline facts not loaded. Set BRAIN_API_KEY or reinstall certainlogic-mcp.",
|
|
241
|
+
confident=False,
|
|
242
|
+
method="error",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# ---- API mode ---------------------------------------------------------
|
|
246
|
+
query_hash = _hash_query(query)
|
|
247
|
+
t_start = time.monotonic()
|
|
248
|
+
method = "error"
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
result = await _call_brain_api_with_retry(resolved_key, query)
|
|
252
|
+
method = result.get("method", "unknown")
|
|
253
|
+
answer = result.get("answer", "")
|
|
254
|
+
confident = method != "uncertain"
|
|
255
|
+
return BrainAPIResult(answer=answer, confident=confident, method=method)
|
|
256
|
+
|
|
257
|
+
except httpx.TimeoutException:
|
|
258
|
+
return BrainAPIResult(
|
|
259
|
+
answer="Brain API timed out. Try again or answer from context.",
|
|
260
|
+
confident=False,
|
|
261
|
+
method="error",
|
|
262
|
+
)
|
|
263
|
+
except httpx.HTTPStatusError as exc:
|
|
264
|
+
msg = _format_http_error(exc.response.status_code)
|
|
265
|
+
return BrainAPIResult(answer=msg, confident=False, method="error")
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
logger.exception("Unexpected error calling Brain API")
|
|
268
|
+
return BrainAPIResult(answer=f"Brain API error: {exc}", confident=False, method="error")
|
|
269
|
+
finally:
|
|
270
|
+
latency_ms = int((time.monotonic() - t_start) * 1000)
|
|
271
|
+
logger.info("[BRAIN_API] ts=%.3f query_hash=%s method=%s latency_ms=%d", time.time(), query_hash, method, latency_ms)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# Tool: batch_query
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
@mcp.tool()
|
|
279
|
+
async def batch_query(
|
|
280
|
+
queries: List[str],
|
|
281
|
+
api_key: Optional[str] = None,
|
|
282
|
+
) -> BatchQueryResult:
|
|
283
|
+
"""Validate multiple facts. Uses same mode (API or offline) as brain_api_query."""
|
|
284
|
+
results = []
|
|
285
|
+
confident_count = 0
|
|
286
|
+
uncertain_count = 0
|
|
287
|
+
error_count = 0
|
|
288
|
+
|
|
289
|
+
for query in queries:
|
|
290
|
+
try:
|
|
291
|
+
result = await brain_api_query(query=query, api_key=api_key)
|
|
292
|
+
results.append(BatchResultItem(query=query, answer=result.answer, confident=result.confident, method=result.method))
|
|
293
|
+
if result.confident:
|
|
294
|
+
confident_count += 1
|
|
295
|
+
elif result.method == "uncertain":
|
|
296
|
+
uncertain_count += 1
|
|
297
|
+
elif result.method == "error":
|
|
298
|
+
error_count += 1
|
|
299
|
+
else:
|
|
300
|
+
uncertain_count += 1
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
logger.exception("Batch query item failed")
|
|
303
|
+
results.append(BatchResultItem(query=query, answer=f"Error: {exc}", confident=False, method="error"))
|
|
304
|
+
error_count += 1
|
|
305
|
+
|
|
306
|
+
return BatchQueryResult(results=results, total=len(results), confident=confident_count, uncertain=uncertain_count, errors=error_count)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Tool: verify_fact_guard
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
@mcp.tool()
|
|
314
|
+
async def verify_fact_guard(
|
|
315
|
+
claim: str,
|
|
316
|
+
source_text: str,
|
|
317
|
+
strictness: float = 0.8,
|
|
318
|
+
api_key: Optional[str] = None,
|
|
319
|
+
) -> GuardResult:
|
|
320
|
+
"""Validate a claim against source text using the hallucination detector.
|
|
321
|
+
|
|
322
|
+
Offline mode: performs a simple substring check (claim in source_text) with configurable strictness.
|
|
323
|
+
API mode: uses the hosted hallucination detector for full semantic analysis.
|
|
324
|
+
"""
|
|
325
|
+
resolved_key = _resolve_api_key(api_key)
|
|
326
|
+
|
|
327
|
+
# ---- OFFLINE mode: simple string containment with keyword heuristics ----
|
|
328
|
+
if not resolved_key:
|
|
329
|
+
claim_lower = claim.lower()
|
|
330
|
+
source_lower = source_text.lower()
|
|
331
|
+
# Simple containment
|
|
332
|
+
if claim_lower in source_lower:
|
|
333
|
+
return GuardResult(valid=True, confidence=0.85, reason="Claim text found in source (offline simple check)", method="offline_simple")
|
|
334
|
+
# Keyword overlap
|
|
335
|
+
claim_words = set(claim_lower.split())
|
|
336
|
+
source_words = set(source_lower.split())
|
|
337
|
+
overlap = len(claim_words & source_words) / max(len(claim_words), 1)
|
|
338
|
+
if overlap >= strictness:
|
|
339
|
+
return GuardResult(valid=True, confidence=overlap, reason=f"{int(overlap*100)}% keyword overlap with source", method="offline_simple")
|
|
340
|
+
return GuardResult(valid=False if overlap < 0.3 else None, confidence=overlap, reason="Claim not found in source (offline mode)", method="offline_simple")
|
|
341
|
+
|
|
342
|
+
# ---- API mode ---------------------------------------------------------
|
|
343
|
+
t_start = time.monotonic()
|
|
344
|
+
try:
|
|
345
|
+
result = await _call_brain_api_with_retry(resolved_key, query=claim, text=source_text, strictness=strictness)
|
|
346
|
+
latency_ms = int((time.monotonic() - t_start) * 1000)
|
|
347
|
+
logger.info("[GUARD] ts=%.3f claim_hash=%s method=%s latency_ms=%d", time.time(), _hash_query(claim), result.get("method", "unknown"), latency_ms)
|
|
348
|
+
return GuardResult(
|
|
349
|
+
valid=result.get("valid"),
|
|
350
|
+
confidence=result.get("confidence", 0.0),
|
|
351
|
+
reason=result.get("reason", ""),
|
|
352
|
+
method=result.get("method", "uncertain"),
|
|
353
|
+
)
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
logger.exception("Guard validation failed")
|
|
356
|
+
return GuardResult(valid=None, confidence=0.0, reason=f"Guard error: {exc}", method="error")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# Tool: health_check
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
@mcp.tool()
|
|
364
|
+
async def health_check() -> HealthResult:
|
|
365
|
+
"""Check server health.
|
|
366
|
+
|
|
367
|
+
Reports whether running in API or OFFLINE mode.
|
|
368
|
+
Offline mode: always healthy if facts are loaded.
|
|
369
|
+
API mode: pings the Brain API health endpoint.
|
|
370
|
+
"""
|
|
371
|
+
# Offline always reports healthy with facts loaded
|
|
372
|
+
if not DEFAULT_API_KEY:
|
|
373
|
+
return HealthResult(
|
|
374
|
+
status="ok",
|
|
375
|
+
mode="offline",
|
|
376
|
+
facts_loaded=len(_OFFLINE_FACTS),
|
|
377
|
+
latency_ms=0,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
t_start = time.monotonic()
|
|
381
|
+
try:
|
|
382
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
383
|
+
response = await client.get(BRAIN_HEALTH_ENDPOINT)
|
|
384
|
+
response.raise_for_status()
|
|
385
|
+
data = response.json()
|
|
386
|
+
latency_ms = int((time.monotonic() - t_start) * 1000)
|
|
387
|
+
return HealthResult(
|
|
388
|
+
status=data.get("status", "ok"),
|
|
389
|
+
mode="api",
|
|
390
|
+
facts_loaded=0, # API mode: facts are server-side
|
|
391
|
+
latency_ms=latency_ms,
|
|
392
|
+
)
|
|
393
|
+
except Exception as exc:
|
|
394
|
+
latency_ms = int((time.monotonic() - t_start) * 1000)
|
|
395
|
+
return HealthResult(
|
|
396
|
+
status="down",
|
|
397
|
+
mode="api",
|
|
398
|
+
facts_loaded=0,
|
|
399
|
+
latency_ms=latency_ms,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
# Internal helpers (unchanged from original)
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
408
|
+
return api_key or DEFAULT_API_KEY
|
|
409
|
+
|
|
410
|
+
def _hash_query(query: str) -> str:
|
|
411
|
+
return hashlib.sha256(query.encode()).hexdigest()[:8]
|
|
412
|
+
|
|
413
|
+
def _format_http_error(status_code: int) -> str:
|
|
414
|
+
if status_code == 401:
|
|
415
|
+
return "Brain API error: Unauthorized (401). Check your API key."
|
|
416
|
+
elif status_code == 429:
|
|
417
|
+
return "Brain API error: Rate limited (429). Retry after a moment."
|
|
418
|
+
elif 500 <= status_code < 600:
|
|
419
|
+
return f"Brain API error: Server error ({status_code}). Retry later."
|
|
420
|
+
else:
|
|
421
|
+
return f"Brain API error: HTTP {status_code}"
|
|
422
|
+
|
|
423
|
+
async def _async_sleep(seconds: float) -> None:
|
|
424
|
+
"""Async sleep — extracted for testability (tests patch this)."""
|
|
425
|
+
import asyncio
|
|
426
|
+
await asyncio.sleep(seconds)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _log(label: str, query: str, method: str) -> None:
|
|
430
|
+
logger.info("[%-9s] query_hash=%s method=%s", label, _hash_query(query), method)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
async def _call_brain_api_with_retry(api_key: str, query: str, text: Optional[str] = None, strictness: Optional[float] = None) -> dict:
|
|
434
|
+
"""POST to Brain API with retry logic."""
|
|
435
|
+
endpoint = BRAIN_VALIDATE_ENDPOINT if text else BRAIN_API_ENDPOINT
|
|
436
|
+
payload: dict = {"query": query}
|
|
437
|
+
if text is not None:
|
|
438
|
+
payload["text"] = text
|
|
439
|
+
if strictness is not None:
|
|
440
|
+
payload["strictness"] = strictness
|
|
441
|
+
|
|
442
|
+
last_exception: Optional[Exception] = None
|
|
443
|
+
for attempt in range(1, MAX_RETRIES + 1):
|
|
444
|
+
try:
|
|
445
|
+
async with httpx.AsyncClient(timeout=BRAIN_API_TIMEOUT) as client:
|
|
446
|
+
response = await client.post(
|
|
447
|
+
endpoint,
|
|
448
|
+
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
|
|
449
|
+
json=payload,
|
|
450
|
+
)
|
|
451
|
+
response.raise_for_status()
|
|
452
|
+
return response.json()
|
|
453
|
+
except httpx.HTTPStatusError as exc:
|
|
454
|
+
if 400 <= exc.response.status_code < 500:
|
|
455
|
+
raise
|
|
456
|
+
last_exception = exc
|
|
457
|
+
if attempt < MAX_RETRIES:
|
|
458
|
+
sleep_s = RETRY_BASE_DELAY * (2 ** (attempt - 1))
|
|
459
|
+
sleep_s += random.uniform(0, RETRY_MAX_JITTER)
|
|
460
|
+
logger.warning("Brain API %d on attempt %d/%d, retrying in %.2fs", exc.response.status_code, attempt, MAX_RETRIES, sleep_s)
|
|
461
|
+
await _async_sleep(sleep_s)
|
|
462
|
+
except (httpx.ConnectError, httpx.ReadError, httpx.WriteError) as exc:
|
|
463
|
+
last_exception = exc
|
|
464
|
+
if attempt < MAX_RETRIES:
|
|
465
|
+
sleep_s = RETRY_BASE_DELAY * (2 ** (attempt - 1))
|
|
466
|
+
sleep_s += random.uniform(0, RETRY_MAX_JITTER)
|
|
467
|
+
logger.warning("Network error attempt %d/%d, retrying in %.2fs: %s", attempt, MAX_RETRIES, sleep_s, exc)
|
|
468
|
+
await _async_sleep(sleep_s)
|
|
469
|
+
if last_exception:
|
|
470
|
+
raise last_exception
|
|
471
|
+
raise RuntimeError("Unexpected: all retries exhausted with no exception")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# Entry point
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
def main() -> None:
|
|
479
|
+
mcp.run()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
if __name__ == "__main__":
|
|
483
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certainlogic-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for CertainLogic Brain API — verified fact lookup for AI agents
|
|
5
|
+
Author: CertainLogic AI
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://certainlogic.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/CertainLogicAI/certainlogic-mcp
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: mcp[cli]>=1.0
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: python-dotenv>=1.0
|
|
14
|
+
Requires-Dist: pydantic>=2.0
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
certainlogic_mcp/__init__.py,sha256=IHbBZrVnkn5a1R5sSrxXl60zdoww_aelB8WJTNus3eo,92
|
|
2
|
+
certainlogic_mcp/free_tier_facts.json,sha256=hnnk1YrSf_5twP4DiY_61xE_eaO2BEKAh62eNHdJNtY,26563
|
|
3
|
+
certainlogic_mcp/server.py,sha256=5Uol0And33dmRejUs3ewTrYbFf_2uE2lqbIpm5VCfB0,18287
|
|
4
|
+
certainlogic_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=zoviqpbN6lCuCEvS0WUfi6XsqAQBo4z4QwFxR11G7k8,1072
|
|
5
|
+
certainlogic_mcp-0.1.0.dist-info/METADATA,sha256=Z1c0ZfQevmdHPzZPYuAugiq95esOnzDVtxDl1oAJ0R4,493
|
|
6
|
+
certainlogic_mcp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
certainlogic_mcp-0.1.0.dist-info/entry_points.txt,sha256=T-FXE5Clb2stjIbUB_VXwWwnCMVcw8DdO_NVpWm8V2E,66
|
|
8
|
+
certainlogic_mcp-0.1.0.dist-info/top_level.txt,sha256=CiqYE3Y6OiAGcYQ7XCJZ3a39V83tfLHuCjoudHVQp-4,17
|
|
9
|
+
certainlogic_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CertainLogic AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
certainlogic_mcp
|