freshcontext-mcp 0.3.18 → 0.3.20

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.
@@ -1,3 +1,4 @@
1
+ import { sanitizeQuery, validateUrl } from "../security.js";
1
2
  /**
2
3
  * Reddit adapter — public JSON API, no auth required.
3
4
  * Accepts subreddit URLs or search queries.
@@ -6,10 +7,15 @@
6
7
  */
7
8
  export async function redditAdapter(options) {
8
9
  let apiUrl = options.url;
9
- // If they pass a plain subreddit name like "r/MachineLearning", build the URL
10
+ // If they pass a plain subreddit or search query, build a Reddit JSON URL.
10
11
  if (!apiUrl.startsWith("http")) {
11
- const clean = apiUrl.replace(/^r\//, "");
12
- apiUrl = `https://www.reddit.com/r/${clean}/.json?limit=25&sort=hot`;
12
+ const clean = sanitizeQuery(apiUrl, 120).replace(/^r\//, "").replace(/^\/+|\/+$/g, "");
13
+ if (/^[A-Za-z0-9_]{2,21}$/.test(clean)) {
14
+ apiUrl = `https://www.reddit.com/r/${clean}/.json?limit=25&sort=hot`;
15
+ }
16
+ else {
17
+ apiUrl = `https://www.reddit.com/search.json?q=${encodeURIComponent(clean)}&sort=new&limit=25`;
18
+ }
13
19
  }
14
20
  // Ensure we hit the JSON endpoint
15
21
  if (!apiUrl.includes(".json")) {
@@ -19,7 +25,8 @@ export async function redditAdapter(options) {
19
25
  if (!apiUrl.includes("limit=")) {
20
26
  apiUrl += (apiUrl.includes("?") ? "&" : "?") + "limit=25";
21
27
  }
22
- const res = await fetch(apiUrl, {
28
+ const safeUrl = validateUrl(apiUrl, "reddit");
29
+ const res = await fetch(safeUrl, {
23
30
  headers: {
24
31
  "User-Agent": "freshcontext-mcp/0.1.5 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
25
32
  "Accept": "application/json",
@@ -22,7 +22,7 @@ export async function repoSearchAdapter(options) {
22
22
  const res = await fetch(apiUrl, {
23
23
  headers: {
24
24
  Accept: "application/vnd.github.v3+json",
25
- "User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
25
+ "User-Agent": "freshcontext-mcp/0.3.19 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
26
26
  },
27
27
  });
28
28
  if (!res.ok) {
@@ -12,7 +12,7 @@
12
12
  */
13
13
  const HEADERS = {
14
14
  "Accept": "application/json",
15
- "User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
15
+ "User-Agent": "freshcontext-mcp/0.3.19 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
16
16
  };
17
17
  async function fetchSecFilings(query, maxResults = 10) {
18
18
  const today = new Date().toISOString().slice(0, 10);
@@ -1,5 +1,13 @@
1
1
  import { calculateFreshnessScore, isMeaningfullyFutureDate, scoreLabel } from "./decay.js";
2
2
  import { looksLikeFailedAdapterContent } from "./guards.js";
3
+ export const MAX_ENVELOPE_CONTENT_LENGTH = 20000;
4
+ function clampEnvelopeMaxLength(maxLength) {
5
+ if (maxLength === 0)
6
+ return 0;
7
+ if (maxLength === undefined || !Number.isFinite(maxLength))
8
+ return 8000;
9
+ return Math.min(MAX_ENVELOPE_CONTENT_LENGTH, Math.max(1, Math.floor(maxLength)));
10
+ }
3
11
  export function stampFreshness(result, options, adapter) {
4
12
  const retrieved_at = new Date().toISOString();
5
13
  const failedContent = looksLikeFailedAdapterContent(result.raw);
@@ -8,7 +16,7 @@ export function stampFreshness(result, options, adapter) {
8
16
  const freshness_confidence = failedContent || futureDated ? "low" : result.freshness_confidence;
9
17
  const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
10
18
  return {
11
- content: result.raw.slice(0, options.maxLength ?? 8000),
19
+ content: result.raw.slice(0, clampEnvelopeMaxLength(options.maxLength)),
12
20
  source_url: options.url,
13
21
  content_date,
14
22
  retrieved_at,
package/dist/security.js CHANGED
@@ -10,9 +10,11 @@ export const ALLOWED_DOMAINS = {
10
10
  yc: ["www.ycombinator.com", "ycombinator.com"],
11
11
  repoSearch: [], // uses GitHub API directly, no browser
12
12
  packageTrends: [], // uses npm/PyPI APIs directly, no browser
13
- reddit: [], // uses public Reddit JSON API, no browser
13
+ reddit: ["www.reddit.com", "reddit.com", "old.reddit.com"],
14
14
  finance: [], // uses Stooq quote API, no browser
15
+ arxiv: ["export.arxiv.org", "arxiv.org"],
15
16
  productHunt: ["www.producthunt.com", "producthunt.com"],
17
+ changelog: [], // accepts public changelog URLs but blocks private/internal targets
16
18
  };
17
19
  // ─── Blocked IP ranges and internal hostnames ────────────────────────────────
18
20
  const BLOCKED_PATTERNS = [
package/dist/server.js CHANGED
@@ -19,12 +19,50 @@ import { secFilingsAdapter } from "./adapters/secFilings.js";
19
19
  import { gdeltAdapter } from "./adapters/gdelt.js";
20
20
  import { gebizAdapter } from "./adapters/gebiz.js";
21
21
  import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
22
+ import { EvaluateContextInputError, evaluateContextInput, formatEvaluateContextResult, } from "./tools/evaluateContext.js";
22
23
  import { formatSecurityError } from "./security.js";
23
24
  const server = new McpServer({
24
25
  name: "freshcontext-mcp",
25
- version: "0.3.18",
26
+ version: "0.3.20",
26
27
  });
27
- // ─── Tool: extract_github ────────────────────────────────────────────────────
28
+ const signalInputSchema = z.object({
29
+ id: z.string().optional(),
30
+ source: z.string().min(1).describe("Source URL, URI, document id, or stable source label."),
31
+ source_type: z.string().optional().describe("Source type such as arxiv, jobs, official_docs, custom, or user_provided."),
32
+ title: z.string().optional(),
33
+ content: z.string().optional(),
34
+ published_at: z.string().nullable().optional(),
35
+ content_date: z.string().nullable().optional(),
36
+ retrieved_at: z.string().nullable().optional(),
37
+ semantic_score: z.number().optional().describe("Optional relevance score from 0..1. Core clamps out-of-range values."),
38
+ date_confidence: z.enum(["high", "medium", "low", "unknown"]).optional(),
39
+ freshness_confidence: z.enum(["high", "medium", "low"]).optional(),
40
+ status: z.enum(["success", "partial", "stale", "failed", "unknown"]).optional(),
41
+ metadata: z.record(z.unknown()).optional(),
42
+ }).passthrough();
43
+ // Tool: evaluate_context
44
+ server.registerTool("evaluate_context", {
45
+ description: "Evaluate caller-provided candidate context and return decision-ready output. This is the primary FreshContext judgment path: it does not fetch, crawl, scrape, browse, read folders, or call adapters.",
46
+ inputSchema: z.object({
47
+ profile: z.string().min(1).describe("Source Profile id, e.g. academic_research, jobs_opportunities, market_finance, official_docs, local_custom."),
48
+ intent: z.string().min(1).describe("Intent Profile id, e.g. citation_check, student_research, developer_adoption, job_search, market_watch, business_due_diligence, medical_literature_triage."),
49
+ signals: z.array(signalInputSchema).min(1).max(100).describe("Candidate context items provided by the caller. FreshContext evaluates these; it does not retrieve them."),
50
+ now: z.string().optional().describe("Optional ISO timestamp for deterministic evaluation."),
51
+ }),
52
+ annotations: { readOnlyHint: true, openWorldHint: false },
53
+ }, async ({ profile, intent, signals, now }) => {
54
+ try {
55
+ const result = evaluateContextInput({ profile, intent, signals, now });
56
+ return { content: [{ type: "text", text: formatEvaluateContextResult(result) }] };
57
+ }
58
+ catch (err) {
59
+ if (err instanceof EvaluateContextInputError) {
60
+ return { content: [{ type: "text", text: `[FreshContext evaluate_context error]\n${err.message}` }] };
61
+ }
62
+ return { content: [{ type: "text", text: formatSecurityError(err) }] };
63
+ }
64
+ });
65
+ // ─── Reference adapter: extract_github ───────────────────────────────────────
28
66
  server.registerTool("extract_github", {
29
67
  description: "Extract real-time data from a GitHub repository — README, stars, forks, language, topics, last commit. Returns timestamped freshcontext.",
30
68
  inputSchema: z.object({
@@ -0,0 +1,146 @@
1
+ import { evaluateSignals, getSourceProfile, interpretEvaluations, } from "../core/index.js";
2
+ const SUPPORTED_INTENTS = [
3
+ "citation_check",
4
+ "student_research",
5
+ "developer_adoption",
6
+ "job_search",
7
+ "market_watch",
8
+ "business_due_diligence",
9
+ "medical_literature_triage",
10
+ ];
11
+ const MAX_CONTEXT_SIGNALS = 100;
12
+ const MAX_SOURCE_CHARS = 2048;
13
+ const MAX_TITLE_CHARS = 1000;
14
+ const MAX_CONTENT_CHARS = 50000;
15
+ export class EvaluateContextInputError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "EvaluateContextInputError";
19
+ }
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ function isIntentProfileId(value) {
25
+ return SUPPORTED_INTENTS.includes(value);
26
+ }
27
+ function assertMaxLength(value, field, maxLength, index) {
28
+ if (typeof value === "string" && value.length > maxLength) {
29
+ const prefix = index === undefined ? "" : `signals[${index}].`;
30
+ throw new EvaluateContextInputError(`${prefix}${field} exceeds maximum length of ${maxLength} characters.`);
31
+ }
32
+ }
33
+ function validateSignal(value, index) {
34
+ if (!isRecord(value)) {
35
+ throw new EvaluateContextInputError(`signals[${index}] must be an object.`);
36
+ }
37
+ if (typeof value.source !== "string" || value.source.trim().length === 0) {
38
+ throw new EvaluateContextInputError(`signals[${index}].source must be a non-empty string.`);
39
+ }
40
+ assertMaxLength(value.source, "source", MAX_SOURCE_CHARS, index);
41
+ assertMaxLength(value.title, "title", MAX_TITLE_CHARS, index);
42
+ assertMaxLength(value.content, "content", MAX_CONTENT_CHARS, index);
43
+ if ((typeof value.title !== "string" || value.title.trim().length === 0)
44
+ && (typeof value.content !== "string" || value.content.trim().length === 0)) {
45
+ throw new EvaluateContextInputError(`signals[${index}] must include title or content.`);
46
+ }
47
+ return {
48
+ ...value,
49
+ source: value.source,
50
+ title: typeof value.title === "string" ? value.title : undefined,
51
+ content: typeof value.content === "string" ? value.content : undefined,
52
+ };
53
+ }
54
+ export function evaluateContextInput(input) {
55
+ const profile = getSourceProfile(input.profile);
56
+ if (!profile) {
57
+ throw new EvaluateContextInputError(`Unknown source profile: ${input.profile}.`);
58
+ }
59
+ if (!isIntentProfileId(input.intent)) {
60
+ throw new EvaluateContextInputError(`Unsupported intent profile: ${input.intent}.`);
61
+ }
62
+ if (!Array.isArray(input.signals)) {
63
+ throw new EvaluateContextInputError("signals must be an array.");
64
+ }
65
+ if (input.signals.length === 0) {
66
+ throw new EvaluateContextInputError("signals must contain at least one candidate context item.");
67
+ }
68
+ if (input.signals.length > MAX_CONTEXT_SIGNALS) {
69
+ throw new EvaluateContextInputError(`signals must contain at most ${MAX_CONTEXT_SIGNALS} candidate context items.`);
70
+ }
71
+ if (input.now !== undefined && Number.isNaN(new Date(input.now).getTime())) {
72
+ throw new EvaluateContextInputError("now must be a valid timestamp string when provided.");
73
+ }
74
+ const signals = input.signals.map(validateSignal);
75
+ const options = input.now ? { now: input.now } : {};
76
+ const evaluations = evaluateSignals(signals, options);
77
+ const decisions = interpretEvaluations(evaluations, {
78
+ sourceProfile: profile,
79
+ intentProfile: input.intent,
80
+ });
81
+ return {
82
+ profile,
83
+ intent: input.intent,
84
+ items: evaluations.map((evaluation, index) => ({
85
+ evaluation,
86
+ decision: decisions[index],
87
+ })),
88
+ };
89
+ }
90
+ function sourceTitle(evaluation) {
91
+ if (evaluation.signal.title)
92
+ return evaluation.signal.title;
93
+ if (evaluation.signal.content)
94
+ return evaluation.signal.content.slice(0, 80);
95
+ return evaluation.signal.source;
96
+ }
97
+ function formatFreshness(score) {
98
+ return score === null ? "unknown" : `${Math.round(score)}/100`;
99
+ }
100
+ function formatRank(score) {
101
+ return score.toFixed(3);
102
+ }
103
+ function formatUtility(score) {
104
+ return `${Number(score.toFixed(1))}/100`;
105
+ }
106
+ function formatList(values) {
107
+ return values.length > 0 ? values.join("; ") : "None";
108
+ }
109
+ export function formatEvaluateContextResult(result) {
110
+ const lines = [
111
+ "FreshContext evaluate_context",
112
+ "Candidate context -> Core evaluation -> decision-ready context",
113
+ "",
114
+ `Profile: ${result.profile.profile_id}`,
115
+ `Purpose: ${result.profile.purpose}`,
116
+ `Intent: ${result.intent}`,
117
+ "",
118
+ ];
119
+ result.items.forEach((item, index) => {
120
+ const { evaluation, decision } = item;
121
+ lines.push(`${index + 1}. ${sourceTitle(evaluation)}`, ` Decision: ${decision.label}`, ` Meaning: ${decision.meaning}`, ` Action: ${decision.action}`, ` Warnings: ${formatList(decision.warnings)}`, ` Source: ${evaluation.signal.source}`, ` Freshness: ${formatFreshness(evaluation.freshness_score)}`, ` Rank score: ${formatRank(evaluation.ranked.final_score)}`, ` Utility: ${formatUtility(evaluation.utility.score)}`, ` Confidence: ${evaluation.ranked.confidence}`, ` Why: ${evaluation.explanation}`, "");
122
+ });
123
+ const structured = {
124
+ profile: result.profile.profile_id,
125
+ intent: result.intent,
126
+ results: result.items.map((item, index) => ({
127
+ index: index + 1,
128
+ title: sourceTitle(item.evaluation),
129
+ source: item.evaluation.signal.source,
130
+ source_type: item.evaluation.signal.source_type,
131
+ decision: item.decision.decision,
132
+ label: item.decision.label,
133
+ meaning: item.decision.meaning,
134
+ action: item.decision.action,
135
+ warnings: item.decision.warnings,
136
+ reasons: item.decision.reasons,
137
+ freshness_score: item.evaluation.freshness_score,
138
+ rank_score: item.evaluation.ranked.final_score,
139
+ utility_score: item.evaluation.utility.score,
140
+ confidence: item.evaluation.ranked.confidence,
141
+ why: item.evaluation.explanation,
142
+ })),
143
+ };
144
+ lines.push("[FRESHCONTEXT_EVALUATION_JSON]", JSON.stringify(structured, null, 2), "[/FRESHCONTEXT_EVALUATION_JSON]");
145
+ return lines.join("\n");
146
+ }
@@ -0,0 +1,166 @@
1
+ # FreshContext Client Setup
2
+
3
+ FreshContext is live as an MCP stdio package on npm.
4
+
5
+ Use this guide when connecting Claude Desktop, Codex, or another MCP-compatible client to the published package.
6
+
7
+ ## What You Should See
8
+
9
+ FreshContext `0.3.19` exposes:
10
+
11
+ ```text
12
+ 22 tools = evaluate_context + 21 read-only reference adapters
13
+ ```
14
+
15
+ The primary interface is:
16
+
17
+ ```text
18
+ evaluate_context
19
+ ```
20
+
21
+ Use it when another retriever, agent, database, note parser, PDF extractor, or local script already has candidate context and needs FreshContext to judge what deserves to reach the model.
22
+
23
+ ## Claude Desktop: Published Package
24
+
25
+ Add this to your Claude Desktop config, then restart Claude.
26
+
27
+ macOS:
28
+
29
+ ```text
30
+ ~/Library/Application Support/Claude/claude_desktop_config.json
31
+ ```
32
+
33
+ Windows:
34
+
35
+ ```text
36
+ %APPDATA%\Claude\claude_desktop_config.json
37
+ ```
38
+
39
+ Config:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "freshcontext": {
45
+ "command": "npx",
46
+ "args": ["-y", "freshcontext-mcp@latest"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ If you previously installed an older global package, refresh it:
53
+
54
+ ```bash
55
+ npm install -g freshcontext-mcp@latest
56
+ ```
57
+
58
+ Then this config is also valid when the global npm bin path is visible to Claude:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "freshcontext": {
64
+ "command": "freshcontext-mcp",
65
+ "args": []
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Codex: Local MCP Config
72
+
73
+ For Codex local MCP config, use the published package through `npx`:
74
+
75
+ ```toml
76
+ [mcp_servers.freshcontext]
77
+ command = "npx"
78
+ args = ["-y", "freshcontext-mcp@latest"]
79
+ ```
80
+
81
+ If you prefer a source checkout while developing FreshContext itself:
82
+
83
+ ```toml
84
+ [mcp_servers.freshcontext]
85
+ command = "node"
86
+ args = ["C:\\Users\\YOUR_USERNAME\\path\\to\\freshcontext-mcp\\dist\\server.js"]
87
+ ```
88
+
89
+ Keep local MCP config files out of git. Do not commit machine-specific paths or credentials.
90
+
91
+ ## Source Checkout Setup
92
+
93
+ Use this when contributing to FreshContext itself:
94
+
95
+ ```bash
96
+ git clone https://github.com/PrinceGabriel-lgtm/freshcontext-mcp
97
+ cd freshcontext-mcp
98
+ npm install
99
+ npm run build
100
+ npm run smoke:stdio
101
+ ```
102
+
103
+ Expected smoke result:
104
+
105
+ ```json
106
+ {
107
+ "ok": true,
108
+ "package_version": "0.3.19",
109
+ "server_version": "0.3.19",
110
+ "tool_count": 22
111
+ }
112
+ ```
113
+
114
+ ## Remote Worker Boundary
115
+
116
+ The repository also declares a remote Streamable HTTP MCP endpoint:
117
+
118
+ ```text
119
+ https://freshcontext-mcp.gimmanuel73.workers.dev/mcp
120
+ ```
121
+
122
+ Some clients can use `mcp-remote`:
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "freshcontext-remote": {
128
+ "command": "npx",
129
+ "args": ["-y", "mcp-remote", "https://freshcontext-mcp.gimmanuel73.workers.dev/mcp"]
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ The npm/local stdio package remains the safest default client path. The hosted Worker endpoint was verified on 2026-06-11 at `0.3.19 / 22 tools` with `evaluate_context` present and returning decision-first output. Because the Worker is a separate deployment surface, re-run remote verification before claiming future package interfaces are live there.
136
+
137
+ ## ChatGPT / OpenAI Connector Boundary
138
+
139
+ Claude and Codex MCP paths are documented now.
140
+
141
+ ChatGPT connector compatibility requires a separate search/fetch compatibility audit before being claimed. Do not assume ChatGPT connector support from Claude/Codex MCP compatibility alone.
142
+
143
+ ## Quick Test Prompt
144
+
145
+ After connecting a client, ask it to use `evaluate_context` with this candidate context:
146
+
147
+ ```json
148
+ {
149
+ "profile": "academic_research",
150
+ "intent": "citation_check",
151
+ "signals": [
152
+ {
153
+ "title": "Fresh research source",
154
+ "content": "A relevant academic source with a reliable publication date.",
155
+ "source": "https://arxiv.org/abs/2605.12345",
156
+ "source_type": "arxiv",
157
+ "published_at": "2026-05-24T12:00:00.000Z",
158
+ "retrieved_at": "2026-05-24T13:00:00.000Z",
159
+ "semantic_score": 0.94,
160
+ "date_confidence": "high"
161
+ }
162
+ ]
163
+ }
164
+ ```
165
+
166
+ Expected result: decision-first output with a decision, meaning, action, warnings, supporting scores, and a structured FreshContext evaluation JSON block.
@@ -12,7 +12,7 @@ The verified local server entrypoint is:
12
12
  & '<node-executable>' '<repo-root>\dist\server.js'
13
13
  ```
14
14
 
15
- The MCP server exposes 21 tools. The local smoke test verifies the package version, server version, expected tool count, and representative tool calls.
15
+ The MCP server exposes 22 tools: the front-door `evaluate_context` tool plus 21 read-only reference adapters. The local smoke test verifies the package version, server version, expected tool count, the generic context-evaluation path, and representative adapter calls.
16
16
 
17
17
  No credential is required for the local stdio smoke path.
18
18
 
@@ -67,7 +67,7 @@ command = "npx"
67
67
  args = ["-y", "mcp-remote", "https://freshcontext-mcp.gimmanuel73.workers.dev/mcp"]
68
68
  ```
69
69
 
70
- This remote path was identified from repository metadata. The validation in this task verified local stdio only, not remote Codex compatibility, Worker availability, or Codex Cloud support.
70
+ This remote path was verified on 2026-06-11 as a live Worker MCP endpoint exposing `0.3.19 / 22 tools`, including `evaluate_context`. That confirms Worker availability and MCP tool discovery. It does not by itself claim Codex Cloud support or guarantee every MCP client can use the remote bridge without its own client-specific setup check.
71
71
 
72
72
  ## Verification steps
73
73
 
@@ -83,9 +83,9 @@ Expected result:
83
83
  ```json
84
84
  {
85
85
  "ok": true,
86
- "package_version": "0.3.18",
87
- "server_version": "0.3.18",
88
- "tool_count": 21
86
+ "package_version": "0.3.19",
87
+ "server_version": "0.3.19",
88
+ "tool_count": 22
89
89
  }
90
90
  ```
91
91
 
@@ -102,7 +102,7 @@ Expected result: no output and exit code 0.
102
102
  - Do not place secrets, credentials, registry tokens, npm tokens, GitHub tokens, or Cloudflare tokens in Codex MCP config.
103
103
  - Do not read, edit, print, or commit local token files, local environment files, registry credentials, Cloudflare local state, or Wrangler state.
104
104
  - Do not commit local Codex config or machine-specific paths.
105
- - Prefer the local stdio path for this compatibility check because it is verified by `npm run smoke:stdio`.
105
+ - Prefer the local stdio path for source-checkout compatibility checks because it is verified by `npm run smoke:stdio`.
106
106
  - Do not claim Codex Cloud support unless it is separately tested and documented.
107
107
 
108
108
  ## Troubleshooting
@@ -112,5 +112,5 @@ If Codex cannot start the server:
112
112
  - Confirm `dist/server.js` exists. If not, run `npm run build`.
113
113
  - Confirm Node is installed with `node -v`. The package requires Node.js 20 or newer.
114
114
  - If `node` is not found by Codex, use the full executable path from `node -p "process.execPath"`.
115
- - Run `npm run smoke:stdio` from the repository root and confirm `tool_count` is 21.
115
+ - Run `npm run smoke:stdio` from the repository root and confirm `tool_count` is 22.
116
116
  - If the remote setup fails, verify network access, `npx` availability, and the remote endpoint separately. Do not treat remote failure as evidence that local stdio is broken.
package/docs/CORE_API.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # FreshContext Core API
2
2
 
3
- FreshContext Core is the reusable engine layer in the current integrated MCP/Core package. It owns signal normalization, envelope creation, freshness scoring, failure honesty, rank/explain primitives, the context-utility primitive, and pure provenance helpers.
3
+ FreshContext Core is the reusable engine layer in the current integrated Core/MCP package. It owns signal normalization, envelope creation, freshness scoring, failure honesty, Source Profiles, decision output, rank/explain primitives, the context-utility primitive, and pure provenance helpers.
4
4
 
5
5
  MCP, Worker HTTP, future REST, and future CLI/SDK surfaces should use Core as the contract center instead of redefining freshness or envelope behavior per host.
6
6
 
7
+ For the package-level boundary between Core, MCP, adapters, and deployment surfaces, see [Core / MCP Boundary](./CORE_MCP_BOUNDARY.md).
8
+
7
9
  ## Stable Public Core API
8
10
 
9
11
  Import stable Core functions from:
@@ -74,9 +76,9 @@ The demo reads caller-provided JSON with `profile`, `intent`, and `signals`, the
74
76
 
75
77
  These types describe the stable envelope and adapter result contract.
76
78
 
77
- ## Signal Contract v1
78
-
79
- Signal Contract v1 is the additive Core shape for a retrieved signal before it is ranked, wrapped, stored, or passed to an agent workflow.
79
+ ## Signal Contract v1
80
+
81
+ Signal Contract v1 is the current FreshContext input standard: the stable shape for candidate context before it is ranked, wrapped, stored, judged by `evaluate_context`, or passed to an agent workflow.
80
82
 
81
83
  Public exports:
82
84
 
@@ -88,9 +90,11 @@ Public exports:
88
90
  - `SignalContractVersion`
89
91
  - `SignalNormalizeOptions`
90
92
 
91
- `published_at` is the canonical signal timestamp. `content_date` is accepted as an adapter/envelope compatibility alias. Normalization clears invalid or meaningfully future-dated timestamps, marks failed/error-looking content as `status: "failed"`, clamps `semantic_score` into `0..1`, and records normalization reasons.
92
-
93
- See [Signal Contract v1](./SIGNAL_CONTRACT.md).
93
+ `published_at` is the canonical signal timestamp. `content_date` is accepted as an adapter/envelope compatibility alias. Normalization clears invalid or meaningfully future-dated timestamps, marks failed/error-looking content as `status: "failed"`, clamps `semantic_score` into `0..1`, and records normalization reasons.
94
+
95
+ Future context signals and control signals are optional future metadata layers, not replacements for Signal Contract v1 and not required public input fields today.
96
+
97
+ See [Signal Contract v1](./SIGNAL_CONTRACT.md).
94
98
 
95
99
  ## Source Profiles
96
100
 
@@ -108,7 +112,7 @@ Public exports:
108
112
  - `SourceFailurePolicy`
109
113
  - `SourceSurface`
110
114
 
111
- They reframe the 21 MCP tools as reference adapters and source-profile examples instead of the product identity. They do not implement `retrieve(...)`, Operator mode, adapter selection, crawling, local file search, or any host/runtime behavior.
115
+ They reframe the 21 named adapter tools as reference adapters and source-profile examples instead of the product identity. The MCP server also exposes `evaluate_context` as the generic caller-provided context evaluation path. Source Profiles do not implement `retrieve(...)`, Operator mode, adapter selection, crawling, local file search, or any host/runtime behavior.
112
116
 
113
117
  ## Decision Helper
114
118
 
@@ -0,0 +1,106 @@
1
+ # FreshContext Core / MCP Boundary
2
+
3
+ FreshContext is the context judgment layer between retrieval and reasoning.
4
+
5
+ The current npm package is intentionally named `freshcontext-mcp` because MCP is the first live interface. That does not make MCP the whole product. MCP is a host layer over the FreshContext Core engine.
6
+
7
+ ## Product Shape
8
+
9
+ ```text
10
+ Candidate context
11
+ -> FreshContext Core
12
+ -> decision-ready context
13
+ -> MCP / Worker / future REST / future SDK / future CLI
14
+ ```
15
+
16
+ FreshContext Core owns the judgment contract:
17
+
18
+ - signal normalization
19
+ - freshness scoring
20
+ - source profiles
21
+ - context utility sidecar scoring
22
+ - rank and explanation primitives
23
+ - decision helper output
24
+ - envelope and provenance helpers
25
+
26
+ MCP owns the live reference interface:
27
+
28
+ - tool registration
29
+ - MCP input schemas
30
+ - stdio transport
31
+ - client-facing tool descriptions
32
+ - formatting Core/adapter output for MCP clients
33
+
34
+ Adapters own source intake:
35
+
36
+ - source-specific fetching
37
+ - source-specific parsing
38
+ - timestamp extraction
39
+ - source-specific normalization
40
+ - failure normalization before Core evaluation
41
+
42
+ Worker/site surfaces own deployment concerns:
43
+
44
+ - Cloudflare runtime behavior
45
+ - KV/D1/cache/feed concerns
46
+ - hosted demo/site presentation
47
+ - runtime guards and deployment configuration
48
+
49
+ ## Current Live Boundary
50
+
51
+ Live today:
52
+
53
+ - npm package: `freshcontext-mcp@0.3.19`
54
+ - MCP stdio server and published binary: `freshcontext-mcp`
55
+ - `evaluate_context` MCP tool for caller-provided candidate context
56
+ - 21 named read-only reference adapters
57
+ - Core signal evaluation
58
+ - Source Profiles
59
+ - Decision Helper
60
+ - adapter registry metadata
61
+ - arXiv signal-to-decision proof
62
+ - bring-your-own-context local demos
63
+ - Trust Scanner release gate
64
+
65
+ Not live today:
66
+
67
+ - standalone Core npm package
68
+ - package rename to `freshcontext`
69
+ - Operator / `retrieve(...)`
70
+ - automatic browser crawling
71
+ - automatic local folder/PDF scanning
72
+ - hosted enterprise dashboard or billing
73
+ - hard Ha-Pri v2 production enforcement
74
+ - full adapter ingestion into pure signal paths
75
+
76
+ ## Future Package Split
77
+
78
+ The safe split path is staged:
79
+
80
+ 1. Keep `freshcontext-mcp` stable for current users.
81
+ 2. Maintain Core as a pure internal export surface.
82
+ 3. Audit Core dependencies, Node/browser compatibility, and API stability.
83
+ 4. Publish a standalone Core package only after compatibility tests exist.
84
+ 5. Make `freshcontext-mcp` depend on the standalone Core package.
85
+ 6. Consider a repo rename to `freshcontext` only after package/client links are stable.
86
+
87
+ The expected future shape is:
88
+
89
+ ```text
90
+ freshcontext
91
+ packages/core reusable judgment engine
92
+ packages/adapters reference source intake assets
93
+ packages/mcp MCP host layer
94
+ packages/cli future local evaluator
95
+ docs
96
+ ```
97
+
98
+ ## Compatibility Rule
99
+
100
+ Do not remove the current compatibility lanes until a dedicated migration pass exists:
101
+
102
+ - `src/types.ts` re-exports legacy adapter types from Core.
103
+ - `src/tools/freshnessStamp.ts` re-exports envelope helpers for older MCP/npm import paths.
104
+ - `dist/server.js` remains the package `main` and MCP binary target.
105
+
106
+ The architecture direction is clear, but the public runtime should stay boring and stable.