standardgraph 1.0.0__tar.gz

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.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: standardgraph
3
+ Version: 1.0.0
4
+ Summary: 20,000+ math standards across 75+ curriculum systems, cross-referenced to CCSS via NLP — accessible as a Claude MCP server
5
+ Author: StandardGraph
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/swoopeagle/standardgraph
8
+ Project-URL: Repository, https://github.com/swoopeagle/standardgraph
9
+ Project-URL: Bug Tracker, https://github.com/swoopeagle/standardgraph/issues
10
+ Keywords: mcp,math,education,curriculum,standards,ccss,claude
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Education
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastmcp>=0.3.0
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: numpy>=1.26.0
22
+
23
+ # standardgraph
24
+
25
+ **20,000+ math standards across 75+ curriculum systems, accessible as a Claude MCP server.**
26
+
27
+ Covers the US (CCSS + all 50 states), Canada, Australia, UK, Singapore, Japan, New Zealand, Ireland, Hong Kong, India, Ghana, South Africa, Rwanda, Cambridge International, IB MYP/DP, and more — all cross-referenced to CCSS via NLP semantic similarity.
28
+
29
+ ## Install (macOS)
30
+
31
+ ```bash
32
+ curl -fsSL https://raw.githubusercontent.com/swoopeagle/standardgraph/main/install.sh | bash
33
+ ```
34
+
35
+ Restart Claude Desktop and look for the hammer 🔨 icon.
36
+
37
+ ## Manual setup
38
+
39
+ ```bash
40
+ mkdir -p ~/.standardgraph
41
+ curl -L https://huggingface.co/datasets/swoopeagle/standardgraph/resolve/main/common_core.db \
42
+ -o ~/.standardgraph/common_core.db
43
+ ```
44
+
45
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "standardgraph": {
51
+ "command": "uvx",
52
+ "args": ["standardgraph"],
53
+ "env": { "DB_PATH": "/Users/YOUR_USERNAME/.standardgraph/common_core.db" }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Tools
60
+
61
+ - `search_standards` — find standards by concept description
62
+ - `lookup_standard` — fetch a standard by ID with prerequisites/successors
63
+ - `get_progression` — trace how a concept develops across grade levels
64
+ - `map_standard` — find the equivalent standard in another curriculum
65
+ - `list_systems` — see all indexed systems with live counts
66
+
67
+ Full documentation: https://github.com/swoopeagle/standardgraph
@@ -0,0 +1,45 @@
1
+ # standardgraph
2
+
3
+ **20,000+ math standards across 75+ curriculum systems, accessible as a Claude MCP server.**
4
+
5
+ Covers the US (CCSS + all 50 states), Canada, Australia, UK, Singapore, Japan, New Zealand, Ireland, Hong Kong, India, Ghana, South Africa, Rwanda, Cambridge International, IB MYP/DP, and more — all cross-referenced to CCSS via NLP semantic similarity.
6
+
7
+ ## Install (macOS)
8
+
9
+ ```bash
10
+ curl -fsSL https://raw.githubusercontent.com/swoopeagle/standardgraph/main/install.sh | bash
11
+ ```
12
+
13
+ Restart Claude Desktop and look for the hammer 🔨 icon.
14
+
15
+ ## Manual setup
16
+
17
+ ```bash
18
+ mkdir -p ~/.standardgraph
19
+ curl -L https://huggingface.co/datasets/swoopeagle/standardgraph/resolve/main/common_core.db \
20
+ -o ~/.standardgraph/common_core.db
21
+ ```
22
+
23
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "standardgraph": {
29
+ "command": "uvx",
30
+ "args": ["standardgraph"],
31
+ "env": { "DB_PATH": "/Users/YOUR_USERNAME/.standardgraph/common_core.db" }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## Tools
38
+
39
+ - `search_standards` — find standards by concept description
40
+ - `lookup_standard` — fetch a standard by ID with prerequisites/successors
41
+ - `get_progression` — trace how a concept develops across grade levels
42
+ - `map_standard` — find the equivalent standard in another curriculum
43
+ - `list_systems` — see all indexed systems with live counts
44
+
45
+ Full documentation: https://github.com/swoopeagle/standardgraph
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "standardgraph"
3
+ version = "1.0.0"
4
+ description = "20,000+ math standards across 75+ curriculum systems, cross-referenced to CCSS via NLP — accessible as a Claude MCP server"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ authors = [{ name = "StandardGraph" }]
9
+ keywords = ["mcp", "math", "education", "curriculum", "standards", "ccss", "claude"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Education",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Education",
17
+ ]
18
+ dependencies = [
19
+ "fastmcp>=0.3.0",
20
+ "httpx>=0.27.0",
21
+ "numpy>=1.26.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/swoopeagle/standardgraph"
26
+ Repository = "https://github.com/swoopeagle/standardgraph"
27
+ "Bug Tracker" = "https://github.com/swoopeagle/standardgraph/issues"
28
+
29
+ [project.scripts]
30
+ standardgraph = "common_core.server:main"
31
+
32
+ [build-system]
33
+ requires = ["setuptools>=61"]
34
+ build-backend = "setuptools.build_meta"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.uv.sources]
40
+ # workspace dep only used in local dev; not needed for PyPI distribution
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,11 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ # For distributed installs (uvx / PyPI), the database lives in ~/.standardgraph/.
5
+ # For local dev, override via the DB_PATH env var.
6
+ DB_PATH = Path(os.getenv("DB_PATH", str(Path.home() / ".standardgraph" / "common_core.db")))
7
+
8
+ # Distributed users run Ollama locally; the 169.254.1.1 Thunderbolt Bridge address
9
+ # is overridden by OLLAMA_BASE_URL in the local dev / overnight pipeline environment.
10
+ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
11
+ EMBED_MODEL = "nomic-embed-text"
@@ -0,0 +1,698 @@
1
+ """International Math Standards MCP server."""
2
+ import json
3
+ import sqlite3
4
+
5
+ import numpy as np
6
+ from fastmcp import FastMCP
7
+
8
+ from common_core.config import DB_PATH, OLLAMA_BASE_URL, EMBED_MODEL
9
+
10
+ def _build_instructions() -> str:
11
+ try:
12
+ _c = sqlite3.connect(DB_PATH)
13
+ _std_count = _c.execute("SELECT COUNT(*) FROM standards").fetchone()[0]
14
+ _sys_count = _c.execute("SELECT COUNT(DISTINCT system) FROM standards").fetchone()[0]
15
+ _c.close()
16
+ except Exception:
17
+ _std_count, _sys_count = 0, 0
18
+ return f"""\
19
+ You have access to a database of {_std_count:,} math standards across {_sys_count} curriculum \
20
+ systems, all cross-referenced to the US Common Core State Standards (CCSS) as the hub.
21
+
22
+ ## Available systems
23
+
24
+ **North America — US:** ccss (hub), plus all 50 states + DC by two-letter code
25
+ (al ak az ar ca co ct dc de fl ga hi id il in ia ks ky la me md ma mi mn ms mo
26
+ mt ne nv nh nj nm ny nc nd oh ok or pa ri sc sd tn tx ut vt va wa wv wi wy)
27
+
28
+ **North America — Canada:** ca-ab (Alberta) ca-bc (British Columbia) ca-mb (Manitoba)
29
+ ca-nb (New Brunswick) ca-on (Ontario) ca-qc (Quebec, French) ca-sk (Saskatchewan)
30
+
31
+ **Asia-Pacific:** sg-moe (Singapore) jp-mext (Japan, Gr 1–6) nz-moe (New Zealand Yr 0–10)
32
+ au-acara (Australian Curriculum) au-vic (Victoria) hk-edb (Hong Kong KS1–3)
33
+ ph-deped (Philippines K–10)
34
+
35
+ **Europe:** uk-nc (England Yr 1–6) uk-aqa (AQA GCSE) gb-sco (Scotland CfE)
36
+ ie-ncca (Ireland Junior Cycle)
37
+
38
+ **South Asia:** in-ncert (India NCERT)
39
+
40
+ **Sub-Saharan Africa:** gh-nacca (Ghana B1–12) za-caps (South Africa Gr R–12)
41
+ rw-reb (Rwanda P4–P6)
42
+
43
+ **International:** cambridge (Cambridge International) ib-myp (IB Middle Years)
44
+ ib-dp (IB Diploma)
45
+
46
+ ## Grade codes
47
+ K, 1, 2, 3, 4, 5, 6, 7, 8, HS
48
+
49
+ ## When to use each tool
50
+
51
+ - **lookup_standard**: user provides a specific standard ID they want to read
52
+ - **search_standards**: user describes a concept and wants to find matching standards
53
+ - **get_progression**: user asks how a topic develops across grade levels
54
+ - **map_standard**: user wants to compare a standard across systems (e.g. "how does Texas
55
+ cover this vs CCSS?" or "what does the IB equivalent look like?")
56
+
57
+ ## Tips
58
+ - Crosswalk mappings are NLP-generated (cosine similarity), not human-verified.
59
+ A confidence ≥ 0.85 is a strong match; 0.70–0.80 is plausible but worth checking.
60
+ A grade_delta ≠ 0 means the systems introduce the concept at different grade levels.
61
+ - map_standard tries three strategies in order: (1) precomputed crosswalk above
62
+ threshold; (2) two-hop CCSS bridge (source→CCSS→target for any-to-any mapping);
63
+ (3) semantic embedding fallback. Below-threshold precomputed results are always
64
+ included, flagged with "below_threshold": true.
65
+ - search_standards queries one system at a time; call it multiple times to compare
66
+ how different curricula cover the same concept.
67
+ """
68
+
69
+
70
+ GRADE_ORDER = ["K", "1", "2", "3", "4", "5", "6", "7", "8", "HS"]
71
+
72
+ # ── System metadata ────────────────────────────────────────────────────────────
73
+ # country_code: ISO 3166-1 alpha-2
74
+ # region: broad geographic grouping used for display and filtering
75
+ # language: primary language of instruction
76
+ # level: national | state | provincial | international | exam_board
77
+
78
+ SYSTEM_META: dict[str, dict] = {
79
+ # ── Hub ──────────────────────────────────────────────────────────────────
80
+ "ccss": {"country": "United States", "country_code": "US", "region": "North America", "language": "English", "level": "national"},
81
+ # ── Canada ───────────────────────────────────────────────────────────────
82
+ "ca-ab": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
83
+ "ca-bc": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
84
+ "ca-mb": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
85
+ "ca-nb": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English/French", "level": "provincial"},
86
+ "ca-on": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
87
+ "ca-qc": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "French", "level": "provincial"},
88
+ "ca-sk": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
89
+ # ── Asia-Pacific ─────────────────────────────────────────────────────────
90
+ "au-acara": {"country": "Australia", "country_code": "AU", "region": "Asia-Pacific", "language": "English", "level": "national"},
91
+ "au-vic": {"country": "Australia", "country_code": "AU", "region": "Asia-Pacific", "language": "English", "level": "state"},
92
+ "hk-edb": {"country": "Hong Kong", "country_code": "HK", "region": "Asia-Pacific", "language": "English/Chinese", "level": "national"},
93
+ "jp-mext": {"country": "Japan", "country_code": "JP", "region": "Asia-Pacific", "language": "Japanese", "level": "national"},
94
+ "nz-moe": {"country": "New Zealand", "country_code": "NZ", "region": "Asia-Pacific", "language": "English", "level": "national"},
95
+ "ph-deped": {"country": "Philippines", "country_code": "PH", "region": "Asia-Pacific", "language": "English/Filipino", "level": "national"},
96
+ "sg-moe": {"country": "Singapore", "country_code": "SG", "region": "Asia-Pacific", "language": "English", "level": "national"},
97
+ # ── Europe ───────────────────────────────────────────────────────────────
98
+ "gb-sco": {"country": "Scotland", "country_code": "GB", "region": "Europe", "language": "English", "level": "national"},
99
+ "ie-ncca": {"country": "Ireland", "country_code": "IE", "region": "Europe", "language": "English/Irish", "level": "national"},
100
+ "uk-aqa": {"country": "England", "country_code": "GB", "region": "Europe", "language": "English", "level": "exam_board"},
101
+ "uk-nc": {"country": "England", "country_code": "GB", "region": "Europe", "language": "English", "level": "national"},
102
+ # ── South Asia ───────────────────────────────────────────────────────────
103
+ "in-ncert": {"country": "India", "country_code": "IN", "region": "South Asia", "language": "English", "level": "national"},
104
+ # ── Sub-Saharan Africa ───────────────────────────────────────────────────
105
+ "gh-nacca": {"country": "Ghana", "country_code": "GH", "region": "Sub-Saharan Africa", "language": "English", "level": "national"},
106
+ "rw-reb": {"country": "Rwanda", "country_code": "RW", "region": "Sub-Saharan Africa", "language": "English/French", "level": "national"},
107
+ "za-caps": {"country": "South Africa", "country_code": "ZA", "region": "Sub-Saharan Africa", "language": "English/Afrikaans", "level": "national"},
108
+ # ── International ────────────────────────────────────────────────────────
109
+ "cambridge": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
110
+ "ib-dp": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
111
+ "ib-myp": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
112
+ }
113
+
114
+ _US_STATE_CODES = {
115
+ "ak","al","ar","az","ca","co","ct","dc","de","fl","ga","hi","ia","id","il",
116
+ "in","ks","ky","la","ma","md","me","mi","mn","mo","ms","mt","nc","nd","ne",
117
+ "nh","nj","nm","nv","ny","oh","ok","or","pa","ri","sc","sd","tn","tx","ut",
118
+ "va","vt","wa","wi","wv","wy",
119
+ }
120
+
121
+ _US_STATE_META = {"country": "United States", "country_code": "US", "region": "North America", "language": "English", "level": "state"}
122
+
123
+
124
+ def _meta(system: str) -> dict:
125
+ if system in SYSTEM_META:
126
+ return SYSTEM_META[system]
127
+ if system in _US_STATE_CODES:
128
+ return _US_STATE_META
129
+ return {}
130
+
131
+
132
+ mcp = FastMCP(
133
+ "intl-math-standards",
134
+ instructions=_build_instructions(),
135
+ )
136
+
137
+
138
+ # ── DB helpers ────────────────────────────────────────────────────────────────
139
+
140
+ def _db() -> sqlite3.Connection:
141
+ conn = sqlite3.connect(DB_PATH)
142
+ conn.row_factory = sqlite3.Row
143
+ conn.execute("PRAGMA foreign_keys = ON")
144
+ return conn
145
+
146
+
147
+ def _expand_id(standard_id: str, system: str = "ccss") -> str:
148
+ """Accept shortform '6.RP.A.3' and expand to 'CCSS.MATH.6.RP.A.3'.
149
+
150
+ Already-qualified IDs (containing '.MATH.' or starting with a
151
+ two-letter state/system prefix like 'TX.') are returned unchanged.
152
+ """
153
+ sid = standard_id.strip()
154
+ upper = sid.upper()
155
+ if upper.startswith("CCSS."):
156
+ return sid
157
+ # Already a qualified non-CCSS ID (e.g. 'TX.MATH.5.3.K', 'FL.MATH.MA.5.NSO.2.5')
158
+ if ".MATH." in upper:
159
+ return sid
160
+ if system == "ccss":
161
+ return f"CCSS.MATH.{sid}"
162
+ return sid
163
+
164
+
165
+ def _grade_key(g: str) -> int:
166
+ try:
167
+ return GRADE_ORDER.index(g)
168
+ except ValueError:
169
+ return 99
170
+
171
+
172
+ # ── Embedding ─────────────────────────────────────────────────────────────────
173
+
174
+ def _embed_query(text: str) -> np.ndarray:
175
+ import httpx
176
+ resp = httpx.post(
177
+ f"{OLLAMA_BASE_URL}/api/embed",
178
+ json={"model": EMBED_MODEL, "input": [text]},
179
+ timeout=30,
180
+ )
181
+ resp.raise_for_status()
182
+ return np.array(resp.json()["embeddings"][0], dtype=np.float32)
183
+
184
+
185
+ def _cosine_scores(query_vec: np.ndarray, conn: sqlite3.Connection) -> list[tuple[float, str]]:
186
+ rows = conn.execute("SELECT standard_id, vector, dimensions FROM embeddings").fetchall()
187
+ if not rows:
188
+ return []
189
+ dim = rows[0]["dimensions"]
190
+ matrix = np.frombuffer(b"".join(r["vector"] for r in rows), dtype=np.float32).reshape(len(rows), dim)
191
+ ids = [r["standard_id"] for r in rows]
192
+
193
+ q = query_vec / (np.linalg.norm(query_vec) + 1e-9)
194
+ norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-9
195
+ scores = (matrix / norms) @ q
196
+
197
+ return sorted(zip(scores.tolist(), ids), reverse=True)
198
+
199
+
200
+ # ── Tool 1: lookup_standard ───────────────────────────────────────────────────
201
+
202
+ @mcp.tool()
203
+ def lookup_standard(
204
+ standard_id: str,
205
+ system: str = "ccss",
206
+ include_elaborations: bool = False,
207
+ ) -> str:
208
+ """Fetch the full text, domain, cluster, prerequisites, and successors for a single standard.
209
+
210
+ Use this when the user provides a specific standard ID they want to read or understand.
211
+
212
+ standard_id: full ID like 'CCSS.MATH.6.RP.A.3' or shortform '6.RP.A.3' (for CCSS).
213
+ For other systems use the full ID, e.g. 'TX.MATH.5.3.K' or 'CA_BC.MATH.3.a'.
214
+ system: curriculum system code (default 'ccss'). See server instructions for all codes.
215
+
216
+ Returns the standard text, grade, domain, cluster, sub-standards (if any),
217
+ prerequisite standard IDs from the prior grade, and successor IDs for the next grade.
218
+ """
219
+ sid = _expand_id(standard_id, system)
220
+ conn = _db()
221
+
222
+ row = conn.execute("SELECT * FROM standards WHERE id = ?", (sid,)).fetchone()
223
+ if not row:
224
+ # Suggest nearby IDs
225
+ suggestions = [
226
+ r[0] for r in conn.execute(
227
+ "SELECT id FROM standards WHERE system=? AND grade=? LIMIT 5",
228
+ (system, sid.split(".")[2] if "." in sid else ""),
229
+ ).fetchall()
230
+ ]
231
+ conn.close()
232
+ return json.dumps({"error": "standard_not_found", "queried_id": sid, "suggestions": suggestions})
233
+
234
+ std = dict(row)
235
+
236
+ sub_stds = conn.execute(
237
+ "SELECT id, text FROM sub_standards WHERE parent_id=? ORDER BY position",
238
+ (sid,),
239
+ ).fetchall()
240
+
241
+ prerequisites = [
242
+ r[0] for r in conn.execute(
243
+ "SELECT target_id FROM standard_relationships WHERE source_id=? AND relationship='prerequisite'",
244
+ (sid,),
245
+ ).fetchall()
246
+ ]
247
+ successors = [
248
+ r[0] for r in conn.execute(
249
+ "SELECT target_id FROM standard_relationships WHERE source_id=? AND relationship='successor'",
250
+ (sid,),
251
+ ).fetchall()
252
+ ]
253
+ conn.close()
254
+
255
+ return json.dumps({
256
+ "id": std["id"],
257
+ "system": std["system"],
258
+ "grade": std["grade"],
259
+ "domain": std["domain"],
260
+ "cluster": std["cluster"],
261
+ "standard_text": std["standard_text"],
262
+ "sub_standards": [f"{r['id']} — {r['text']}" for r in sub_stds],
263
+ "prerequisites": prerequisites,
264
+ "successors": successors,
265
+ "source_url": std["source_url"],
266
+ "elaborations": None,
267
+ }, indent=2)
268
+
269
+
270
+ # ── Tool 2: search_standards ──────────────────────────────────────────────────
271
+
272
+ @mcp.tool()
273
+ def search_standards(
274
+ query: str,
275
+ system: str = "ccss",
276
+ grade: str | None = None,
277
+ domain: str | None = None,
278
+ limit: int = 5,
279
+ ) -> str:
280
+ """Find math standards that match a natural language description of a concept or skill.
281
+
282
+ Use this when the user describes what they're looking for rather than citing a standard ID.
283
+ Examples: "adding fractions with unlike denominators", "geometric transformations grade 8",
284
+ "solving quadratic equations".
285
+
286
+ query: plain English description of the math concept or skill.
287
+ system: which curriculum to search (default 'ccss'). Call multiple times to compare systems.
288
+ grade: optional filter — single grade '5', range '6-8', or 'HS'. Grade codes: K 1 2 3 4 5 6 7 8 HS.
289
+ domain: optional keyword to restrict by domain name (e.g. 'geometry', 'algebra').
290
+ limit: number of results (default 5, max sensible ~10).
291
+
292
+ Returns standards ranked by semantic similarity with relevance scores (0–1).
293
+ """
294
+ query_vec = _embed_query(query)
295
+ conn = _db()
296
+ scored = _cosine_scores(query_vec, conn)
297
+
298
+ results = []
299
+ for score, sid in scored:
300
+ if len(results) >= limit:
301
+ break
302
+ row = conn.execute(
303
+ "SELECT * FROM standards WHERE id=? AND system=?", (sid, system)
304
+ ).fetchone()
305
+ if not row:
306
+ continue
307
+ std = dict(row)
308
+
309
+ if grade is not None:
310
+ # Accept "6", "6-8", or ["5","6","7"]
311
+ grades_wanted = _parse_grade_filter(grade)
312
+ if std["grade"] not in grades_wanted:
313
+ continue
314
+
315
+ if domain is not None and domain.lower() not in std["domain"].lower():
316
+ continue
317
+
318
+ results.append({
319
+ "id": std["id"],
320
+ "grade": std["grade"],
321
+ "domain": std["domain"],
322
+ "standard_text": std["standard_text"],
323
+ "relevance_score": round(score, 4),
324
+ })
325
+
326
+ conn.close()
327
+ return json.dumps(results, indent=2)
328
+
329
+
330
+ def _parse_grade_filter(grade: str | list) -> set[str]:
331
+ if isinstance(grade, list):
332
+ return set(grade)
333
+ if "-" in grade and not grade.startswith("K"):
334
+ # range like "6-8"
335
+ parts = grade.split("-")
336
+ try:
337
+ lo, hi = int(parts[0]), int(parts[1])
338
+ return {str(g) for g in range(lo, hi + 1)}
339
+ except ValueError:
340
+ pass
341
+ return {grade}
342
+
343
+
344
+ # ── Tool 3: get_progression ───────────────────────────────────────────────────
345
+
346
+ @mcp.tool()
347
+ def get_progression(
348
+ concept: str,
349
+ system: str = "ccss",
350
+ grade_start: int | None = None,
351
+ grade_end: int | None = None,
352
+ ) -> str:
353
+ """Show how a math concept is introduced and built upon across grade levels.
354
+
355
+ Use this when the user asks questions like "how does fractions develop from grade 3 to 6?"
356
+ or "what's the full progression for proportional reasoning?" or "when is X introduced?"
357
+
358
+ concept: plain English name of the math concept (e.g. 'fractions', 'linear equations',
359
+ 'place value', 'geometric transformations').
360
+ system: curriculum to trace (default 'ccss'). Try 'cambridge' or 'ib-myp' for comparison.
361
+ grade_start / grade_end: optional integer bounds to narrow the range (e.g. 3 and 8).
362
+
363
+ Returns the top matching standards per grade, ordered K through HS, showing how the
364
+ concept deepens over time.
365
+ """
366
+ query_vec = _embed_query(concept)
367
+ conn = _db()
368
+ scored = _cosine_scores(query_vec, conn)
369
+
370
+ # Collect top standards per grade, filtered by grade range
371
+ by_grade: dict[str, list[dict]] = {}
372
+ for score, sid in scored:
373
+ if score < 0.5:
374
+ break
375
+ row = conn.execute(
376
+ "SELECT * FROM standards WHERE id=? AND system=?", (sid, system)
377
+ ).fetchone()
378
+ if not row:
379
+ continue
380
+ std = dict(row)
381
+ g = std["grade"]
382
+
383
+ if grade_start is not None and _grade_key(g) < _grade_key(str(grade_start)):
384
+ continue
385
+ if grade_end is not None and _grade_key(g) > _grade_key(str(grade_end)):
386
+ continue
387
+
388
+ by_grade.setdefault(g, []).append({
389
+ "id": std["id"],
390
+ "text": std["standard_text"],
391
+ "score": round(score, 4),
392
+ })
393
+
394
+ conn.close()
395
+
396
+ gr_start = str(grade_start) if grade_start is not None else "K"
397
+ gr_end = str(grade_end) if grade_end is not None else "HS"
398
+
399
+ stages = []
400
+ for g in sorted(by_grade.keys(), key=_grade_key):
401
+ stds = sorted(by_grade[g], key=lambda x: -x["score"])[:3]
402
+ stages.append({
403
+ "grade": g,
404
+ "standards": [{"id": s["id"], "text": s["text"]} for s in stds],
405
+ })
406
+
407
+ return json.dumps({
408
+ "concept": concept,
409
+ "system": system,
410
+ "grade_range": f"{gr_start}–{gr_end}",
411
+ "stages": stages,
412
+ }, indent=2)
413
+
414
+
415
+ # ── Tool 4: map_standard ──────────────────────────────────────────────────────
416
+
417
+ @mcp.tool()
418
+ def map_standard(
419
+ standard_id: str,
420
+ from_system: str,
421
+ to_system: str,
422
+ confidence_threshold: float = 0.7,
423
+ ) -> str:
424
+ """Find the closest equivalent to a standard in a different curriculum system.
425
+
426
+ Use this when the user wants to compare curricula — e.g. "what is the CCSS equivalent
427
+ of this Texas standard?", "how does Singapore cover this?", or
428
+ "I'm moving from Ontario to the UK — what's the equivalent?"
429
+
430
+ Three strategies are tried in order:
431
+ 1. Precomputed NLP crosswalk (direct or reverse through CCSS hub).
432
+ 2. Two-hop CCSS bridge: source→CCSS→target (enables any-to-any comparison).
433
+ 3. Semantic embedding fallback: embed source text, find nearest in target system.
434
+ Below-threshold precomputed results are always returned, flagged with below_threshold.
435
+
436
+ standard_id: the source standard ID (full form, e.g. 'TX.MATH.5.3.K').
437
+ from_system: system code of the source standard (e.g. 'tx', 'ca-on', 'sg-moe').
438
+ to_system: target system code (any indexed system).
439
+ confidence_threshold: minimum cosine similarity for primary results (default 0.7).
440
+
441
+ Returns matched standards with confidence score, grade alignment, and mapping method.
442
+ """
443
+ sid = _expand_id(standard_id, from_system)
444
+ conn = _db()
445
+
446
+ src = conn.execute("SELECT * FROM standards WHERE id=?", (sid,)).fetchone()
447
+ if not src:
448
+ conn.close()
449
+ return json.dumps({"error": "standard_not_found", "queried_id": sid})
450
+
451
+ src_dict = dict(src)
452
+
453
+ # ── 1. Precomputed crosswalk above threshold ───────────────────────────────
454
+ mappings = conn.execute(
455
+ """SELECT cm.*, s.standard_text AS target_text, s.grade AS target_grade,
456
+ s.domain AS target_domain
457
+ FROM crosswalk_mappings cm
458
+ JOIN standards s ON s.id = cm.target_id
459
+ WHERE cm.source_id = ?
460
+ AND cm.target_system = ?
461
+ AND cm.confidence_score >= ?
462
+ ORDER BY cm.confidence_score DESC""",
463
+ (sid, to_system, confidence_threshold),
464
+ ).fetchall()
465
+
466
+ if mappings:
467
+ conn.close()
468
+ return json.dumps({
469
+ "source_id": src_dict["id"],
470
+ "target_curriculum": to_system,
471
+ "mapping_method": "precomputed_crosswalk",
472
+ "mappings": [
473
+ {
474
+ "target_id": m["target_id"],
475
+ "target_standard_text": m["target_text"],
476
+ "relationship": m["relationship"],
477
+ "confidence": m["confidence_score"],
478
+ "grade_delta": m["grade_delta"],
479
+ "grade_alignment": "exact" if m["grade_delta"] == 0 else (
480
+ f"{abs(m['grade_delta'])} year{'s' if abs(m['grade_delta']) > 1 else ''} "
481
+ f"{'later' if m['grade_delta'] > 0 else 'earlier'} in target"
482
+ ),
483
+ "verified_by_human": bool(m["verified_by_human"]),
484
+ "notes": m["notes"],
485
+ }
486
+ for m in mappings
487
+ ],
488
+ }, indent=2)
489
+
490
+ # ── 2. Best precomputed result below threshold ─────────────────────────────
491
+ best_below = conn.execute(
492
+ """SELECT cm.*, s.standard_text AS target_text, s.grade AS target_grade,
493
+ s.domain AS target_domain
494
+ FROM crosswalk_mappings cm
495
+ JOIN standards s ON s.id = cm.target_id
496
+ WHERE cm.source_id = ?
497
+ AND cm.target_system = ?
498
+ ORDER BY cm.confidence_score DESC
499
+ LIMIT 1""",
500
+ (sid, to_system),
501
+ ).fetchone()
502
+
503
+ # ── 3. Two-hop: source → CCSS → target (or reverse for CCSS sources) ──────
504
+ two_hop: list[dict] = []
505
+ if from_system == "ccss":
506
+ # Source IS the CCSS hub — find target-system standards pointing to it
507
+ rows = conn.execute(
508
+ """SELECT cm.source_id, cm.confidence_score,
509
+ s.standard_text, s.grade, s.domain
510
+ FROM crosswalk_mappings cm
511
+ JOIN standards s ON s.id = cm.source_id
512
+ WHERE cm.target_id = ?
513
+ AND s.system = ?
514
+ ORDER BY cm.confidence_score DESC
515
+ LIMIT 5""",
516
+ (sid, to_system),
517
+ ).fetchall()
518
+ for r in rows:
519
+ two_hop.append({
520
+ "target_id": r["source_id"],
521
+ "target_standard_text": r["standard_text"],
522
+ "via_ccss": sid,
523
+ "hop1_confidence": 1.0,
524
+ "hop2_confidence": round(r["confidence_score"], 4),
525
+ "combined_confidence": round(r["confidence_score"], 4),
526
+ "grade": r["grade"],
527
+ })
528
+ elif to_system != "ccss":
529
+ # Forward two-hop: source → CCSS intermediary → target
530
+ ccss_rows = conn.execute(
531
+ """SELECT target_id, confidence_score
532
+ FROM crosswalk_mappings
533
+ WHERE source_id = ? AND target_system = 'ccss'
534
+ ORDER BY confidence_score DESC
535
+ LIMIT 3""",
536
+ (sid,),
537
+ ).fetchall()
538
+ raw: list[dict] = []
539
+ for cr in ccss_rows:
540
+ ccss_id, ccss_conf = cr["target_id"], cr["confidence_score"]
541
+ target_rows = conn.execute(
542
+ """SELECT cm.source_id, cm.confidence_score,
543
+ s.standard_text, s.grade, s.domain
544
+ FROM crosswalk_mappings cm
545
+ JOIN standards s ON s.id = cm.source_id
546
+ WHERE cm.target_id = ?
547
+ AND s.system = ?
548
+ ORDER BY cm.confidence_score DESC
549
+ LIMIT 3""",
550
+ (ccss_id, to_system),
551
+ ).fetchall()
552
+ for tr in target_rows:
553
+ raw.append({
554
+ "target_id": tr["source_id"],
555
+ "target_standard_text": tr["standard_text"],
556
+ "via_ccss": ccss_id,
557
+ "hop1_confidence": round(ccss_conf, 4),
558
+ "hop2_confidence": round(tr["confidence_score"], 4),
559
+ "combined_confidence": round(ccss_conf * tr["confidence_score"], 4),
560
+ "grade": tr["grade"],
561
+ })
562
+ seen: set[str] = set()
563
+ for r in sorted(raw, key=lambda x: -x["combined_confidence"]):
564
+ if r["target_id"] not in seen:
565
+ seen.add(r["target_id"])
566
+ two_hop.append(r)
567
+ if len(two_hop) >= 5:
568
+ break
569
+
570
+ # ── 4. Semantic embedding fallback ────────────────────────────────────────
571
+ nearest_by_concept: list[dict] = []
572
+ try:
573
+ qvec = _embed_query(src_dict["standard_text"])
574
+ scored = _cosine_scores(qvec, conn)
575
+ for score, candidate_id in scored:
576
+ if len(nearest_by_concept) >= 3:
577
+ break
578
+ if score < 0.35:
579
+ break
580
+ row = conn.execute(
581
+ "SELECT * FROM standards WHERE id=? AND system=?", (candidate_id, to_system)
582
+ ).fetchone()
583
+ if row:
584
+ nearest_by_concept.append({
585
+ "target_id": row["id"],
586
+ "target_standard_text": row["standard_text"],
587
+ "grade": row["grade"],
588
+ "semantic_similarity": round(score, 4),
589
+ })
590
+ except Exception:
591
+ pass
592
+
593
+ conn.close()
594
+
595
+ # ── Build no-match response ────────────────────────────────────────────────
596
+ response: dict = {
597
+ "source_id": src_dict["id"],
598
+ "source_text": src_dict["standard_text"],
599
+ "target_curriculum": to_system,
600
+ "result": "no_precomputed_mapping_above_threshold",
601
+ }
602
+
603
+ if best_below:
604
+ response["best_precomputed_below_threshold"] = {
605
+ "target_id": best_below["target_id"],
606
+ "target_standard_text": best_below["target_text"],
607
+ "confidence": round(best_below["confidence_score"], 4),
608
+ "below_threshold": True,
609
+ "threshold_used": confidence_threshold,
610
+ }
611
+
612
+ if two_hop:
613
+ response["two_hop_via_ccss"] = two_hop
614
+
615
+ if nearest_by_concept:
616
+ response["nearest_by_concept"] = nearest_by_concept
617
+
618
+ if not best_below and not two_hop and not nearest_by_concept:
619
+ response["result"] = "no_mapping_found"
620
+ try:
621
+ _c = sqlite3.connect(DB_PATH)
622
+ response["available_systems"] = [
623
+ r[0] for r in _c.execute(
624
+ "SELECT DISTINCT system FROM standards ORDER BY system"
625
+ ).fetchall()
626
+ ]
627
+ _c.close()
628
+ except Exception:
629
+ pass
630
+
631
+ return json.dumps(response, indent=2)
632
+
633
+
634
+ # ── Tool 5: list_systems ──────────────────────────────────────────────────────
635
+
636
+ @mcp.tool()
637
+ def list_systems() -> str:
638
+ """Return a live count of every curriculum system currently in the database.
639
+
640
+ Use this to see exactly which systems are available, how many standards each has,
641
+ and overall DB stats. Unlike the server instructions (which are set at startup),
642
+ this always reflects the current state of the database.
643
+
644
+ Returns system codes, standard counts, embedding coverage, and crosswalk coverage.
645
+ """
646
+ conn = _db()
647
+
648
+ systems = conn.execute(
649
+ """SELECT s.system,
650
+ COUNT(s.id) AS standards,
651
+ COUNT(e.standard_id) AS embedded,
652
+ COUNT(cm.source_id) AS crosswalked
653
+ FROM standards s
654
+ LEFT JOIN embeddings e ON e.standard_id = s.id
655
+ LEFT JOIN crosswalk_mappings cm ON cm.source_id = s.id
656
+ GROUP BY s.system
657
+ ORDER BY s.system"""
658
+ ).fetchall()
659
+
660
+ total_std = conn.execute("SELECT COUNT(*) FROM standards").fetchone()[0]
661
+ total_emb = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
662
+ total_xwalk = conn.execute("SELECT COUNT(*) FROM crosswalk_mappings").fetchone()[0]
663
+ total_rel = conn.execute("SELECT COUNT(*) FROM standard_relationships").fetchone()[0]
664
+ conn.close()
665
+
666
+ system_rows = []
667
+ for r in systems:
668
+ m = _meta(r["system"])
669
+ system_rows.append({
670
+ "system": r["system"],
671
+ "standards": r["standards"],
672
+ "embedded": r["embedded"],
673
+ "crosswalked": r["crosswalked"],
674
+ "country": m.get("country"),
675
+ "country_code": m.get("country_code"),
676
+ "region": m.get("region"),
677
+ "language": m.get("language"),
678
+ "level": m.get("level"),
679
+ })
680
+
681
+ return json.dumps({
682
+ "totals": {
683
+ "systems": len(systems),
684
+ "standards": total_std,
685
+ "embeddings": total_emb,
686
+ "crosswalk_mappings": total_xwalk,
687
+ "relationships": total_rel,
688
+ },
689
+ "systems": system_rows,
690
+ }, indent=2)
691
+
692
+
693
+ def main() -> None:
694
+ mcp.run()
695
+
696
+
697
+ if __name__ == "__main__":
698
+ main()
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: standardgraph
3
+ Version: 1.0.0
4
+ Summary: 20,000+ math standards across 75+ curriculum systems, cross-referenced to CCSS via NLP — accessible as a Claude MCP server
5
+ Author: StandardGraph
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/swoopeagle/standardgraph
8
+ Project-URL: Repository, https://github.com/swoopeagle/standardgraph
9
+ Project-URL: Bug Tracker, https://github.com/swoopeagle/standardgraph/issues
10
+ Keywords: mcp,math,education,curriculum,standards,ccss,claude
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Education
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastmcp>=0.3.0
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: numpy>=1.26.0
22
+
23
+ # standardgraph
24
+
25
+ **20,000+ math standards across 75+ curriculum systems, accessible as a Claude MCP server.**
26
+
27
+ Covers the US (CCSS + all 50 states), Canada, Australia, UK, Singapore, Japan, New Zealand, Ireland, Hong Kong, India, Ghana, South Africa, Rwanda, Cambridge International, IB MYP/DP, and more — all cross-referenced to CCSS via NLP semantic similarity.
28
+
29
+ ## Install (macOS)
30
+
31
+ ```bash
32
+ curl -fsSL https://raw.githubusercontent.com/swoopeagle/standardgraph/main/install.sh | bash
33
+ ```
34
+
35
+ Restart Claude Desktop and look for the hammer 🔨 icon.
36
+
37
+ ## Manual setup
38
+
39
+ ```bash
40
+ mkdir -p ~/.standardgraph
41
+ curl -L https://huggingface.co/datasets/swoopeagle/standardgraph/resolve/main/common_core.db \
42
+ -o ~/.standardgraph/common_core.db
43
+ ```
44
+
45
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "standardgraph": {
51
+ "command": "uvx",
52
+ "args": ["standardgraph"],
53
+ "env": { "DB_PATH": "/Users/YOUR_USERNAME/.standardgraph/common_core.db" }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Tools
60
+
61
+ - `search_standards` — find standards by concept description
62
+ - `lookup_standard` — fetch a standard by ID with prerequisites/successors
63
+ - `get_progression` — trace how a concept develops across grade levels
64
+ - `map_standard` — find the equivalent standard in another curriculum
65
+ - `list_systems` — see all indexed systems with live counts
66
+
67
+ Full documentation: https://github.com/swoopeagle/standardgraph
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/common_core/__init__.py
4
+ src/common_core/config.py
5
+ src/common_core/server.py
6
+ src/standardgraph.egg-info/PKG-INFO
7
+ src/standardgraph.egg-info/SOURCES.txt
8
+ src/standardgraph.egg-info/dependency_links.txt
9
+ src/standardgraph.egg-info/entry_points.txt
10
+ src/standardgraph.egg-info/requires.txt
11
+ src/standardgraph.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ standardgraph = common_core.server:main
@@ -0,0 +1,3 @@
1
+ fastmcp>=0.3.0
2
+ httpx>=0.27.0
3
+ numpy>=1.26.0
@@ -0,0 +1 @@
1
+ common_core