open-primitive-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Open Primitive Protocol (OPP)
2
+
3
+ **The data layer of the agent internet.**
4
+
5
+ OPP defines how data providers make their data agent-consumable, verifiable, and discoverable. Three components: provider manifest, response envelope, query interface.
6
+
7
+ ## The Problem
8
+
9
+ Agents get data naked. No provenance. No freshness guarantee. No confidence score. No way to verify the source. The agent internet is missing its data envelope.
10
+
11
+ ## The Protocol
12
+
13
+ | Component | What it does |
14
+ |-----------|-------------|
15
+ | **Manifest** (`/.well-known/opp.json`) | Declares what data a provider serves, how fresh it is, how to verify it |
16
+ | **Response Envelope** | Every response carries domain, source, freshness, confidence, citation, Ed25519 signature |
17
+ | **Query Interface** | Standardized HTTP GET endpoints with predictable parameters |
18
+
19
+ ## Conformance Levels
20
+
21
+ - **Level 1 (Basic):** Manifest + envelope with domain/source/freshness
22
+ - **Level 2 (Cited):** Level 1 + confidence scores + citations
23
+ - **Level 3 (Verified):** Level 2 + Ed25519 cryptographic proof
24
+
25
+ ## Reference Implementation
26
+
27
+ Live at [api.openprimitive.com](https://api.openprimitive.com). 16 US federal data domains across 11 agencies. Level 3 compliant — every response is signed.
28
+
29
+ ```bash
30
+ curl https://api.openprimitive.com/v1/drugs?name=aspirin
31
+ # Returns OPP envelope with proof.type: "DataIntegrityProof"
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ **Use data from an OPP provider:**
37
+ ```javascript
38
+ const res = await fetch('https://api.openprimitive.com/v1/drugs?name=aspirin');
39
+ const data = await res.json();
40
+ console.log(data.citations.statement);
41
+ // "According to FDA FAERS, aspirin has 601,477 reported adverse events"
42
+ ```
43
+
44
+ **Implement OPP for your own data:**
45
+ ```javascript
46
+ // 1. Create /.well-known/opp.json (see spec)
47
+ // 2. Wrap responses in the OPP envelope
48
+ // 3. Optionally sign with Ed25519
49
+ ```
50
+
51
+ ## Specification
52
+
53
+ Full spec: [openprimitive.com/protocol.html](https://openprimitive.com/protocol.html)
54
+
55
+ Detailed spec: [api.openprimitive.com/spec.html](https://api.openprimitive.com/spec.html)
56
+
57
+ ## SDK
58
+
59
+ - `sdk/opp-client.js` — Client library for consuming OPP providers
60
+ - `sdk/opp-provider.js` — Helper for implementing OPP
61
+ - `sdk/opp-validator.js` — Validate OPP conformance
62
+
63
+ ## MCP Server
64
+
65
+ 13 tools for Claude, Cursor, and MCP-compatible agents:
66
+
67
+ ```bash
68
+ npx open-primitive-mcp
69
+ ```
70
+
71
+ ## Links
72
+
73
+ - [Protocol Spec](https://openprimitive.com/protocol.html)
74
+ - [API Reference](https://api.openprimitive.com)
75
+ - [Manifesto](https://openprimitive.com/manifesto.html)
76
+ - [OpenAPI Spec](https://api.openprimitive.com/openapi.json)
77
+ - [OPP Manifest](https://api.openprimitive.com/.well-known/opp.json)
78
+
79
+ ## License
80
+
81
+ MIT
package/mcp.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
3
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
4
+ const { z } = require('zod');
5
+
6
+ const flights = require('./sources/flights');
7
+ const cars = require('./sources/cars');
8
+ const food = require('./sources/food');
9
+ const water = require('./sources/water');
10
+ const drugs = require('./sources/drugs');
11
+ const hospitals = require('./sources/hospitals');
12
+ const health = require('./sources/health');
13
+ const nutrition = require('./sources/nutrition');
14
+ const jobs = require('./sources/jobs');
15
+ const demographics = require('./sources/demographics');
16
+ const products = require('./sources/products');
17
+ const sec = require('./sources/sec');
18
+ const safety = require('./sources/safety');
19
+
20
+ const server = new McpServer({
21
+ name: 'open-primitive',
22
+ version: '1.0.0',
23
+ });
24
+
25
+ // ─── FLIGHTS ───
26
+ server.registerTool('get-flights', {
27
+ title: 'Get Flight Status',
28
+ description: 'Get live delay and weather data for 8 major US airlines. Source: FAA NAS + Open-Meteo.',
29
+ inputSchema: z.object({
30
+ airline: z.string().optional().describe('IATA code (DL, UA, AA, WN, AS, B6, G4, F9). Omit for all airlines.'),
31
+ }),
32
+ }, async ({ airline }) => {
33
+ const data = airline ? await flights.getAirline(airline) : await flights.getAirlines();
34
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
35
+ });
36
+
37
+ // ─── CARS ───
38
+ server.registerTool('get-car-safety', {
39
+ title: 'Get Car Safety',
40
+ description: 'Get NHTSA crash safety ratings and recalls for a vehicle. Source: NHTSA.',
41
+ inputSchema: z.object({
42
+ year: z.string().describe('Model year (e.g. "2024")'),
43
+ make: z.string().describe('Manufacturer (e.g. "Toyota")'),
44
+ model: z.string().describe('Model name (e.g. "Camry")'),
45
+ }),
46
+ }, async ({ year, make, model }) => {
47
+ const data = await cars.getSafety(year, make, model);
48
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
49
+ });
50
+
51
+ // ─── FOOD ───
52
+ server.registerTool('get-food-recalls', {
53
+ title: 'Get Food Recalls',
54
+ description: 'Get active FDA food recalls or search by product/brand. Source: FDA Enforcement.',
55
+ inputSchema: z.object({
56
+ query: z.string().optional().describe('Search term (product or brand). Omit for recent recalls.'),
57
+ }),
58
+ }, async ({ query }) => {
59
+ const data = query ? await food.search(query) : await food.getRecent();
60
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
61
+ });
62
+
63
+ // ─── WATER ───
64
+ server.registerTool('get-water-safety', {
65
+ title: 'Get Water Safety',
66
+ description: 'Get drinking water system data and violations. Source: EPA SDWIS.',
67
+ inputSchema: z.object({
68
+ zip: z.string().optional().describe('5-digit ZIP code to find water systems'),
69
+ pwsid: z.string().optional().describe('Public Water System ID for detailed violations'),
70
+ }),
71
+ }, async ({ zip, pwsid }) => {
72
+ const data = pwsid ? await water.getSystem(pwsid) : await water.searchByZip(zip || '');
73
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
74
+ });
75
+
76
+ // ─── DRUGS ───
77
+ server.registerTool('get-drug-safety', {
78
+ title: 'Get Drug Safety',
79
+ description: 'Get FDA adverse event reports, top reactions, and label warnings for a drug. Source: FDA FAERS.',
80
+ inputSchema: z.object({
81
+ name: z.string().describe('Drug name (brand or generic, e.g. "ibuprofen")'),
82
+ }),
83
+ }, async ({ name }) => {
84
+ const data = await drugs.getDrug(name);
85
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
86
+ });
87
+
88
+ // ─── HOSPITALS ───
89
+ server.registerTool('get-hospital-quality', {
90
+ title: 'Get Hospital Quality',
91
+ description: 'Search hospitals or get detailed quality ratings. Source: CMS Care Compare.',
92
+ inputSchema: z.object({
93
+ query: z.string().optional().describe('Hospital name or ZIP code to search'),
94
+ providerId: z.string().optional().describe('CMS Provider ID for detailed quality data'),
95
+ }),
96
+ }, async ({ query, providerId }) => {
97
+ const data = providerId ? await hospitals.getHospital(providerId) : await hospitals.searchHospitals(query || '');
98
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
99
+ });
100
+
101
+ // ─── HEALTH ───
102
+ server.registerTool('get-health-evidence', {
103
+ title: 'Get Health Evidence',
104
+ description: 'Search PubMed for research evidence on supplements or health claims. Source: PubMed/MEDLINE.',
105
+ inputSchema: z.object({
106
+ query: z.string().describe('Supplement name or health claim (e.g. "vitamin d", "turmeric inflammation")'),
107
+ }),
108
+ }, async ({ query }) => {
109
+ const data = await health.searchHealth(query);
110
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
111
+ });
112
+
113
+ // ─── NUTRITION ───
114
+ server.registerTool('get-nutrition', {
115
+ title: 'Get Nutrition Data',
116
+ description: 'Search USDA FoodData Central for nutrition facts or get details by FDC ID. Source: USDA.',
117
+ inputSchema: z.object({
118
+ query: z.string().optional().describe('Food search term (e.g. "banana", "cheddar cheese"). Omit if using fdcId.'),
119
+ fdcId: z.string().optional().describe('FDC ID for a specific food item. Omit if using query.'),
120
+ }),
121
+ }, async ({ query, fdcId }) => {
122
+ const data = fdcId ? await nutrition.getFood(fdcId) : await nutrition.searchFood(query || '');
123
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
124
+ });
125
+
126
+ // ─── JOBS ───
127
+ server.registerTool('get-jobs', {
128
+ title: 'Get Jobs Data',
129
+ description: 'Get unemployment rate or other BLS time series data. Source: Bureau of Labor Statistics.',
130
+ inputSchema: z.object({
131
+ seriesId: z.string().optional().describe('BLS series ID (e.g. "LNS14000000" for unemployment rate). Omit for default unemployment data.'),
132
+ }),
133
+ }, async ({ seriesId }) => {
134
+ const data = seriesId ? await jobs.getSeriesData(seriesId) : await jobs.getUnemployment();
135
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
136
+ });
137
+
138
+ // ─── DEMOGRAPHICS ───
139
+ server.registerTool('get-demographics', {
140
+ title: 'Get Demographics',
141
+ description: 'Get Census demographics for a ZIP code: population, income, poverty, education, housing. Source: US Census ACS.',
142
+ inputSchema: z.object({
143
+ zip: z.string().describe('5-digit ZIP code'),
144
+ }),
145
+ }, async ({ zip }) => {
146
+ const data = await demographics.getByZip(zip);
147
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
148
+ });
149
+
150
+ // ─── PRODUCT RECALLS ───
151
+ server.registerTool('get-product-recalls', {
152
+ title: 'Get Product Recalls',
153
+ description: 'Get recent CPSC consumer product recalls or search by keyword. Source: SaferProducts.gov.',
154
+ inputSchema: z.object({
155
+ query: z.string().optional().describe('Search term (product type or brand). Omit for recent recalls.'),
156
+ }),
157
+ }, async ({ query }) => {
158
+ const data = query ? await products.search(query) : await products.getRecent();
159
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
160
+ });
161
+
162
+ // ─── SEC FILINGS ───
163
+ server.registerTool('get-sec-filings', {
164
+ title: 'Get SEC Filings',
165
+ description: 'Search SEC EDGAR for company filings or get structured financial facts by CIK. Source: SEC EDGAR.',
166
+ inputSchema: z.object({
167
+ query: z.string().optional().describe('Company name to search (e.g. "Apple"). Omit if using cik.'),
168
+ cik: z.string().optional().describe('SEC CIK number for detailed company facts. Omit if using query.'),
169
+ }),
170
+ }, async ({ query, cik }) => {
171
+ const data = cik ? await sec.getCompanyFacts(cik) : await sec.searchCompany(query || '');
172
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
173
+ });
174
+
175
+ // ─── SAFETY PROFILE ───
176
+ server.registerTool('get-safety-profile', {
177
+ title: 'Get Safety Profile',
178
+ description: 'Get a cross-domain safety composite for a ZIP code combining water quality and hospital ratings. Source: EPA + CMS.',
179
+ inputSchema: z.object({
180
+ zip: z.string().describe('5-digit ZIP code'),
181
+ }),
182
+ }, async ({ zip }) => {
183
+ const data = await safety.getSafetyProfile(zip);
184
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
185
+ });
186
+
187
+ // Start
188
+ async function main() {
189
+ const transport = new StdioServerTransport();
190
+ await server.connect(transport);
191
+ }
192
+
193
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "open-primitive-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for 13 federal data domains — flights, cars, food, water, drugs, hospitals, health, nutrition, jobs, demographics, products, SEC, safety",
5
+ "main": "mcp.js",
6
+ "bin": {
7
+ "open-primitive-mcp": "./mcp.js"
8
+ },
9
+ "files": ["mcp.js", "sources/", "package.json"],
10
+ "keywords": ["mcp", "model-context-protocol", "federal-data", "government", "ai-agents", "open-primitive"],
11
+ "author": "David Hamilton <davehamiltonj@gmail.com>",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/writesdavid/open-primitive-api"
16
+ },
17
+ "dependencies": {
18
+ "node-fetch": "^2.7.0",
19
+ "@modelcontextprotocol/sdk": "^1.27.0",
20
+ "zod": "^3.24.0"
21
+ }
22
+ }
@@ -0,0 +1,190 @@
1
+ const { Redis } = require('@upstash/redis');
2
+ const food = require('./food');
3
+ const products = require('./products');
4
+
5
+ const ALERT_KEY = 'alerts:feed';
6
+ const SNAPSHOT_KEY = 'alerts:snapshot';
7
+ const MAX_ALERTS = 50;
8
+
9
+ let redis = null;
10
+
11
+ function getRedis() {
12
+ if (!redis && process.env.UPSTASH_REDIS_REST_URL) {
13
+ redis = new Redis({
14
+ url: process.env.UPSTASH_REDIS_REST_URL,
15
+ token: process.env.UPSTASH_REDIS_REST_TOKEN,
16
+ });
17
+ }
18
+ return redis;
19
+ }
20
+
21
+ // ─── ALERT RULES ───
22
+ // Each rule: { domain, match(item) => alert | null }
23
+
24
+ const FOOD_RULES = {
25
+ domain: 'food',
26
+ extract(recalls) {
27
+ return recalls
28
+ .filter(r => r.classification === 'Class I')
29
+ .map(r => ({
30
+ domain: 'food',
31
+ event: 'class_i_recall',
32
+ severity: 'critical',
33
+ title: `Class I Recall: ${(r.firm || 'Unknown firm').slice(0, 80)}`,
34
+ summary: (r.reason || '').slice(0, 300),
35
+ data: {
36
+ product: r.product,
37
+ firm: r.firm,
38
+ recallNumber: r.recallNumber,
39
+ date: r.date,
40
+ distribution: r.distribution,
41
+ },
42
+ endpoint: '/v1/food',
43
+ timestamp: new Date().toISOString(),
44
+ }));
45
+ },
46
+ fingerprint(item) {
47
+ return `food:${item.data.recallNumber || item.title}`;
48
+ },
49
+ };
50
+
51
+ const PRODUCTS_RULES = {
52
+ domain: 'products',
53
+ extract(recalls) {
54
+ const childKeywords = /child|infant|baby|toddler|kid|crib|stroller|toy|pacifier|nursery/i;
55
+ return recalls
56
+ .filter(r => {
57
+ const text = `${r.title} ${r.description} ${(r.products || []).map(p => p.name).join(' ')}`;
58
+ return childKeywords.test(text);
59
+ })
60
+ .map(r => ({
61
+ domain: 'products',
62
+ event: 'children_product_recall',
63
+ severity: 'critical',
64
+ title: `Children's Product Recall: ${(r.title || '').slice(0, 80)}`,
65
+ summary: (r.description || '').slice(0, 300),
66
+ data: {
67
+ recallId: r.recallId,
68
+ products: r.products,
69
+ hazards: r.hazards,
70
+ manufacturers: r.manufacturers,
71
+ recallDate: r.recallDate,
72
+ url: r.url,
73
+ },
74
+ endpoint: '/v1/products',
75
+ timestamp: new Date().toISOString(),
76
+ }));
77
+ },
78
+ fingerprint(item) {
79
+ return `products:${item.data.recallId || item.title}`;
80
+ },
81
+ };
82
+
83
+ // ─── CORE ───
84
+
85
+ async function getSnapshot() {
86
+ const r = getRedis();
87
+ if (!r) return {};
88
+ const raw = await r.get(SNAPSHOT_KEY);
89
+ if (!raw) return {};
90
+ return typeof raw === 'string' ? JSON.parse(raw) : raw;
91
+ }
92
+
93
+ async function saveSnapshot(snapshot) {
94
+ const r = getRedis();
95
+ if (!r) return;
96
+ await r.set(SNAPSHOT_KEY, JSON.stringify(snapshot));
97
+ }
98
+
99
+ async function pushAlerts(newAlerts) {
100
+ const r = getRedis();
101
+ if (!r) return;
102
+ const raw = await r.get(ALERT_KEY);
103
+ let existing = [];
104
+ if (raw) {
105
+ existing = typeof raw === 'string' ? JSON.parse(raw) : raw;
106
+ }
107
+ const merged = [...newAlerts, ...existing].slice(0, MAX_ALERTS);
108
+ await r.set(ALERT_KEY, JSON.stringify(merged));
109
+ }
110
+
111
+ async function checkForAlerts() {
112
+ const snapshot = await getSnapshot();
113
+ const newAlerts = [];
114
+
115
+ // Food: Class I recalls
116
+ try {
117
+ const foodData = await food.getRecent();
118
+ if (foodData.recalls && !foodData.error) {
119
+ const candidates = FOOD_RULES.extract(foodData.recalls);
120
+ for (const alert of candidates) {
121
+ const fp = FOOD_RULES.fingerprint(alert);
122
+ if (!snapshot[fp]) {
123
+ newAlerts.push(alert);
124
+ snapshot[fp] = Date.now();
125
+ }
126
+ }
127
+ }
128
+ } catch (err) {
129
+ console.error('Alert check failed for food:', err.message);
130
+ }
131
+
132
+ // Products: children's recalls
133
+ try {
134
+ const productsData = await products.getRecent();
135
+ if (productsData.recalls && !productsData.error) {
136
+ const candidates = PRODUCTS_RULES.extract(productsData.recalls);
137
+ for (const alert of candidates) {
138
+ const fp = PRODUCTS_RULES.fingerprint(alert);
139
+ if (!snapshot[fp]) {
140
+ newAlerts.push(alert);
141
+ snapshot[fp] = Date.now();
142
+ }
143
+ }
144
+ }
145
+ } catch (err) {
146
+ console.error('Alert check failed for products:', err.message);
147
+ }
148
+
149
+ // Prune snapshot entries older than 90 days
150
+ const cutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
151
+ for (const key of Object.keys(snapshot)) {
152
+ if (snapshot[key] < cutoff) delete snapshot[key];
153
+ }
154
+
155
+ if (newAlerts.length > 0) {
156
+ await pushAlerts(newAlerts);
157
+ }
158
+ await saveSnapshot(snapshot);
159
+
160
+ return newAlerts;
161
+ }
162
+
163
+ async function getAlertFeed() {
164
+ const r = getRedis();
165
+
166
+ // No Redis: run a live check and return what we find
167
+ if (!r) {
168
+ const live = await checkForAlerts();
169
+ return {
170
+ alerts: live.slice(0, MAX_ALERTS),
171
+ count: live.length,
172
+ lastUpdated: new Date().toISOString(),
173
+ note: 'Live check — no persistent store configured',
174
+ };
175
+ }
176
+
177
+ const raw = await r.get(ALERT_KEY);
178
+ let alerts = [];
179
+ if (raw) {
180
+ alerts = typeof raw === 'string' ? JSON.parse(raw) : raw;
181
+ }
182
+
183
+ return {
184
+ alerts,
185
+ count: alerts.length,
186
+ lastUpdated: alerts.length > 0 ? alerts[0].timestamp : null,
187
+ };
188
+ }
189
+
190
+ module.exports = { checkForAlerts, getAlertFeed };