@nikeandocean/carbon-factor-matcher 0.2.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,252 @@
1
+ # Carbon Factor Matcher — MCP Server for Carbon Accounting
2
+
3
+ An MCP (Model Context Protocol) server that connects LLMs with carbon footprint databases. It provides intelligent emission factor matching for carbon accounting, LCA (Life Cycle Assessment), and ESG reporting applications.
4
+
5
+ ## What is this MCP?
6
+
7
+ Carbon Factor Matcher helps AI agents and LLM-based applications find the most appropriate emission factors from standardized environmental databases. It uses a two-stage hybrid search algorithm:
8
+
9
+ 1. **Embedding-based rough filtering** — Semantic similarity to find candidate factors
10
+ 2. **LLM-based fine ranking** — AI-powered selection with reasoning and confidence scores
11
+
12
+ ### Supported Databases
13
+
14
+ - **ELCD** — European Reference Life Cycle Database (~600 factors, included in Free tier)
15
+ - **ecoinvent 3.10** — Swiss Centre for Life Cycle Inventories (~21,000 factors, Pro license required)
16
+
17
+ ### Key Features
18
+
19
+ - 5-dimension data quality rating (technology, geography, source, time, factor type)
20
+ - Multi-language support (Chinese/English activity descriptions)
21
+ - MCP-compatible — works with Claude, Cursor, Windsurf, Cline, Continue, and any MCP client
22
+
23
+ ## Installation
24
+
25
+ ### Claude Code
26
+
27
+ ```bash
28
+ claude mcp add carbon-factor-matcher -- npx -y @nikeandocean/carbon-factor-matcher
29
+ ```
30
+
31
+ ### Claude Desktop
32
+
33
+ Add to `claude_desktop_config.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "carbon-factor-matcher": {
39
+ "command": "npx",
40
+ "args": ["-y", "@nikeandocean/carbon-factor-matcher"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ Config file locations:
47
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
48
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
49
+
50
+ ### Cursor
51
+
52
+ Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "carbon-factor-matcher": {
58
+ "command": "npx",
59
+ "args": ["-y", "@nikeandocean/carbon-factor-matcher"]
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Or via Cursor UI: **Settings → MCP → Add new global MCP server**.
66
+
67
+ ### Windsurf
68
+
69
+ Add to `~/.codeium/windsurf/mcp_config.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "carbon-factor-matcher": {
75
+ "command": "npx",
76
+ "args": ["-y", "@nikeandocean/carbon-factor-matcher"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Or via Windsurf UI: **Settings → Cascade → MCP Servers → Add**.
83
+
84
+ ### Cline (VS Code Extension)
85
+
86
+ Add to Cline MCP settings (click **MCP Servers** icon in Cline panel):
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "carbon-factor-matcher": {
92
+ "command": "npx",
93
+ "args": ["-y", "@nikeandocean/carbon-factor-matcher"]
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Continue (VS Code / JetBrains)
100
+
101
+ Add to `~/.continue/config.yaml`:
102
+
103
+ ```yaml
104
+ mcpServers:
105
+ - name: carbon-factor-matcher
106
+ command: npx
107
+ args:
108
+ - "-y"
109
+ - "@nikeandocean/carbon-factor-matcher"
110
+ ```
111
+
112
+ ### Smithery.ai (One-Click Install)
113
+
114
+ ```bash
115
+ npx -y smithery mcp add nikeandocean/carbon-factor-matcher
116
+ ```
117
+
118
+ Or visit [smithery.ai/server/@nikeandocean/carbon-factor-matcher](https://smithery.ai/server/@nikeandocean/carbon-factor-matcher) for hosted endpoint.
119
+
120
+ ## Pro License
121
+
122
+ The Free tier works immediately after installation (300 queries/day, ELCD database, keyword search). To unlock the full experience:
123
+
124
+ | Plan | Price | Features |
125
+ |------|-------|----------|
126
+ | **Free** | $0 | ELCD database (~600 factors), keyword search, 300 queries/day |
127
+ | **Pro** | $5 (one-time) | ELCD + ecoinvent (~21,000 factors), LLM-powered matching, unlimited queries, data quality rating |
128
+
129
+ ### Purchase License Key
130
+
131
+ 👉 **[Buy Pro License](https://nikeandocean.github.io/carbon-factor-matcher)**
132
+
133
+ After purchase, you'll receive a license key via email. Set it as an environment variable in your MCP config:
134
+
135
+ ```json
136
+ {
137
+ "mcpServers": {
138
+ "carbon-factor-matcher": {
139
+ "command": "npx",
140
+ "args": ["-y", "@nikeandocean/carbon-factor-matcher"],
141
+ "env": {
142
+ "CARBON_FACTOR_LICENSE_KEY": "PRO-xxxx-xxxx",
143
+ "LLM_API_KEY": "sk-xxxx",
144
+ "LLM_BASE_URL": "https://api.deepseek.com",
145
+ "LLM_MODEL": "deepseek-chat"
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ | Variable | Description | Default |
155
+ |----------|-------------|---------|
156
+ | `CARBON_FACTOR_LICENSE_KEY` | Your license key (empty = Free tier) | — |
157
+ | `LLM_API_KEY` | LLM API key for Pro matching (DeepSeek/OpenAI) | — |
158
+ | `LLM_BASE_URL` | LLM endpoint URL | `https://api.deepseek.com` |
159
+ | `LLM_MODEL` | LLM model name | `deepseek-chat` |
160
+ | `CARBON_FACTOR_DATA_DIR` | Path to local factor database | `data/factors` |
161
+
162
+ ## Available Tools
163
+
164
+ ### `factor_match`
165
+
166
+ Match activity data to the best emission factor. Free tier uses keyword search; Pro tier uses hybrid embedding + LLM matching with quality assessment.
167
+
168
+ **Input:**
169
+ ```json
170
+ {
171
+ "activity_data": "Factory in Shenzhen, 10kV industrial electricity, 2024, semiconductor fab",
172
+ "top_k": 10
173
+ }
174
+ ```
175
+
176
+ **Output:**
177
+ ```json
178
+ {
179
+ "selected_factor": {
180
+ "id": "elec-cn-south-10kv-2024",
181
+ "name": "Electricity, 10kV, South China Grid",
182
+ "value": 0.6101,
183
+ "unit": "kgCO2e/kWh"
184
+ },
185
+ "confidence": 0.92,
186
+ "reason": "Best match for industrial electricity in South China region",
187
+ "alternatives": [...]
188
+ }
189
+ ```
190
+
191
+ ### `factor_search`
192
+
193
+ Search emission factors by keyword with optional filters.
194
+
195
+ **Input:**
196
+ ```json
197
+ {
198
+ "query": "diesel",
199
+ "category": "fuel",
200
+ "limit": 10
201
+ }
202
+ ```
203
+
204
+ ### `factor_detail`
205
+
206
+ Get full metadata for a specific factor.
207
+
208
+ **Input:**
209
+ ```json
210
+ {
211
+ "factor_id": "elec-cn-south-10kv-2024"
212
+ }
213
+ ```
214
+
215
+ ## System Requirements
216
+
217
+ - Node.js 18+ (for `npx`)
218
+ - Python 3.11+ (auto-installed via pip)
219
+ - LLM API key (Pro tier only — DeepSeek recommended)
220
+
221
+ ## Tech Stack
222
+
223
+ - **MCP SDK** — Model Context Protocol implementation
224
+ - **Sentence Transformers** — Semantic embedding (shibing624/text2vec-base-chinese)
225
+ - **DeepSeek/OpenAI** — LLM-based factor ranking
226
+ - **SQLite** — Usage tracking
227
+ - **Cloudflare Workers** — API and webhook processing
228
+ - **Paddle** — Payment processing
229
+
230
+ ## Discovery
231
+
232
+ This MCP server is available on:
233
+
234
+ | Platform | Link | Status |
235
+ |----------|------|--------|
236
+ | **npm** | [@nikeandocean/carbon-factor-matcher](https://www.npmjs.com/package/@nikeandocean/carbon-factor-matcher) | Published |
237
+ | **Smithery.ai** | [smithery.ai/server/@nikeandocean/carbon-factor-matcher](https://smithery.ai/server/@nikeandocean/carbon-factor-matcher) | Listed |
238
+ | **mcp.so** | [mcp.so](https://mcp.so) | Submit via site |
239
+ | **Glama.ai** | [glama.ai/mcp/servers](https://glama.ai/mcp/servers) | Auto-indexed from npm |
240
+
241
+ ## Support
242
+
243
+ - **Email:** tao.yan@zju.edu.cn
244
+ - **Issues:** https://github.com/tra121vel/carbon-factor-matcher/issues
245
+
246
+ ## License
247
+
248
+ This is proprietary software. See [LICENSE](LICENSE) for details.
249
+
250
+ ---
251
+
252
+ © 2024 Carbon Factor Matcher. All rights reserved.
package/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Carbon Factor Matcher - MCP Server
5
+ *
6
+ * npm wrapper that launches the Python MCP server.
7
+ * Requires: Python 3.11+, pip install carbon-factor-matcher
8
+ */
9
+
10
+ const { execSync, spawn } = require('child_process');
11
+
12
+ // Check if Python is available
13
+ try {
14
+ execSync('python --version', { stdio: 'ignore' });
15
+ } catch (e) {
16
+ console.error('Error: Python 3.11+ is required to run carbon-factor-matcher');
17
+ console.error('Install from https://www.python.org/downloads/');
18
+ process.exit(1);
19
+ }
20
+
21
+ // Launch the MCP server
22
+ const child = spawn('python', ['-m', 'carbon_factor_matcher'], {
23
+ stdio: 'inherit',
24
+ });
25
+
26
+ child.on('exit', (code) => {
27
+ process.exit(code || 0);
28
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@nikeandocean/carbon-factor-matcher",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for intelligent emission factor matching — connects LLMs with carbon emission factor databases (ELCD, ecoinvent)",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "carbon-factor-matcher": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "src/carbon_factor_matcher/**/*.py",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "mcp-server",
21
+ "carbon",
22
+ "emission-factor",
23
+ "carbon-accounting",
24
+ "lca",
25
+ "life-cycle-assessment",
26
+ "ecoinvent",
27
+ "elcd",
28
+ "sustainability",
29
+ "climate",
30
+ "esg",
31
+ "ghg",
32
+ "carbon-footprint",
33
+ "co2"
34
+ ],
35
+ "author": "tra121vel",
36
+ "license": "Proprietary",
37
+ "dependencies": {},
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ }
41
+ }
@@ -0,0 +1,3 @@
1
+ """Carbon Factor Matcher - MCP server for intelligent emission factor matching."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m carbon_factor_matcher`."""
2
+
3
+ from carbon_factor_matcher.server import main
4
+
5
+ main()
@@ -0,0 +1,6 @@
1
+ """Factor database adapters for different data source formats."""
2
+
3
+ from carbon_factor_matcher.adapters.base import FactorAdapter
4
+ from carbon_factor_matcher.adapters.ecoinvent import EcoinventExcelAdapter
5
+
6
+ __all__ = ["FactorAdapter", "EcoinventExcelAdapter"]
@@ -0,0 +1,22 @@
1
+ """Base class for factor database adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from carbon_factor_matcher.models import Factor
6
+
7
+
8
+ class FactorAdapter(ABC):
9
+ """Abstract base class for loading emission factors from different data sources.
10
+
11
+ Each adapter knows how to parse a specific format (ELCD JSON-LD,
12
+ ecoinvent Excel, EcoSpold2 XML, etc.) and return a list of Factor objects.
13
+ """
14
+
15
+ @abstractmethod
16
+ def load(self) -> list[Factor]:
17
+ """Load all factors from the data source.
18
+
19
+ Returns:
20
+ List of Factor objects ready for matching.
21
+ """
22
+ ...
@@ -0,0 +1,292 @@
1
+ """Adapter for ecoinvent Database Overview Excel files.
2
+
3
+ Loads activity metadata from the ecoinvent Excel export and converts
4
+ them into Factor objects for matching. CO2 values are placeholders (0.0)
5
+ until Phase 2 fills them from the actual database.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from carbon_factor_matcher.adapters.base import FactorAdapter
13
+ from carbon_factor_matcher.models import Factor
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class ImportResult:
20
+ """Result of an ecoinvent import operation."""
21
+ total_rows: int = 0
22
+ imported: int = 0
23
+ skipped: int = 0
24
+ errors: list[str] = field(default_factory=list)
25
+
26
+ @property
27
+ def success_rate(self) -> float:
28
+ """Calculate import success rate."""
29
+ if self.total_rows == 0:
30
+ return 0.0
31
+ return self.imported / self.total_rows
32
+
33
+ # Sheet names for different system models
34
+ SYSTEM_MODEL_SHEETS = {
35
+ "cut-off": "Cut-Off AO",
36
+ "cutoff": "Cut-Off AO",
37
+ "undefined": "Undefined AO",
38
+ "apos": "APOS AO",
39
+ "consequential": "Consequential AO",
40
+ "en15804": "EN15804 AO",
41
+ }
42
+
43
+ # Sectors to skip (not useful for emission factor matching)
44
+ SKIP_SECTORS = {
45
+ "Waste Treatment & Recycling",
46
+ }
47
+
48
+
49
+ class EcoinventExcelAdapter(FactorAdapter):
50
+ """Load ecoinvent factors from the Database Overview Excel file.
51
+
52
+ Args:
53
+ excel_path: Path to the ecoinvent Excel file.
54
+ system_model: System model to load. One of: "cut-off", "undefined",
55
+ "apos", "consequential", "en15804". Default: "cut-off".
56
+ sectors: Optional list of sectors to include. If None, includes all.
57
+ geographies: Optional list of geography codes to include. If None, includes all.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ excel_path: str,
63
+ system_model: str = "cut-off",
64
+ sectors: list[str] | None = None,
65
+ geographies: list[str] | None = None,
66
+ ):
67
+ self._path = Path(excel_path)
68
+ if not self._path.exists():
69
+ raise FileNotFoundError(f"Excel file not found: {excel_path}")
70
+
71
+ sheet_name = SYSTEM_MODEL_SHEETS.get(system_model.lower())
72
+ if sheet_name is None:
73
+ raise ValueError(
74
+ f"Unknown system model '{system_model}'. "
75
+ f"Choose from: {list(SYSTEM_MODEL_SHEETS.keys())}"
76
+ )
77
+ self._sheet_name = sheet_name
78
+ self._sectors = set(sectors) if sectors else None
79
+ self._geographies = set(geographies) if geographies else None
80
+
81
+ def load(self) -> list[Factor]:
82
+ """Load factors from the Excel file."""
83
+ try:
84
+ import openpyxl
85
+ except ImportError:
86
+ raise ImportError(
87
+ "openpyxl is required for ecoinvent Excel adapter. "
88
+ "Install it with: pip install openpyxl"
89
+ )
90
+
91
+ logger.info(f"Loading ecoinvent from {self._path.name}, sheet={self._sheet_name}")
92
+ wb = openpyxl.load_workbook(self._path, read_only=True, data_only=True)
93
+
94
+ if self._sheet_name not in wb.sheetnames:
95
+ wb.close()
96
+ raise ValueError(
97
+ f"Sheet '{self._sheet_name}' not found. "
98
+ f"Available: {wb.sheetnames}"
99
+ )
100
+
101
+ ws = wb[self._sheet_name]
102
+ headers = [cell.value for cell in next(ws.iter_rows(min_row=1, max_row=1))]
103
+ col_map = {h: i for i, h in enumerate(headers) if h}
104
+
105
+ factors = []
106
+ skipped = 0
107
+
108
+ for row in ws.iter_rows(min_row=2, values_only=True):
109
+ row_list = list(row)
110
+
111
+ # Extract fields by header name
112
+ activity_uuid = self._get_col(row_list, col_map, "Activity UUID")
113
+ activity_name = self._get_col(row_list, col_map, "Activity Name")
114
+ geography = self._get_col(row_list, col_map, "Geography")
115
+ time_period = self._get_col(row_list, col_map, "Time Period")
116
+ sector = self._get_col(row_list, col_map, "Sector")
117
+ product_name = self._get_col(row_list, col_map, "Reference Product Name")
118
+ unit = self._get_col(row_list, col_map, "Unit")
119
+ isic = self._get_col(row_list, col_map, "ISIC Classification")
120
+
121
+ # Skip if missing essential fields
122
+ if not activity_uuid or not activity_name:
123
+ skipped += 1
124
+ continue
125
+
126
+ # Skip waste/infrastructure sectors
127
+ if sector in SKIP_SECTORS:
128
+ skipped += 1
129
+ continue
130
+
131
+ # Apply filters
132
+ if self._sectors and sector not in self._sectors:
133
+ skipped += 1
134
+ continue
135
+ if self._geographies and geography not in self._geographies:
136
+ skipped += 1
137
+ continue
138
+
139
+ # Parse year from time period (e.g., "2019 - 2023" → 2019)
140
+ source_year = self._parse_year(time_period)
141
+
142
+ # Build applicability from product info
143
+ applicability = product_name or activity_name
144
+
145
+ # Build category from sector + ISIC
146
+ category = sector or ""
147
+ if isic:
148
+ category = f"{sector}/{isic}" if sector else isic
149
+
150
+ factor = Factor(
151
+ id=activity_uuid,
152
+ name=activity_name,
153
+ category=category,
154
+ value=0.0, # Placeholder — Phase 2 fills CO2 values
155
+ unit=unit or "",
156
+ geography={"location": geography or ""},
157
+ applicability=applicability,
158
+ source="ecoinvent 3.10",
159
+ source_year=source_year,
160
+ raw_data={
161
+ "product_name": product_name,
162
+ "isic": isic,
163
+ "time_period": time_period,
164
+ "system_model": self._sheet_name,
165
+ },
166
+ )
167
+ factors.append(factor)
168
+
169
+ wb.close()
170
+ logger.info(f"Loaded {len(factors)} ecoinvent factors (skipped {skipped})")
171
+ return factors
172
+
173
+ def load_with_result(self) -> tuple[list[Factor], ImportResult]:
174
+ """Load factors and return detailed import result.
175
+
176
+ Returns:
177
+ Tuple of (factors_list, import_result).
178
+ """
179
+ try:
180
+ import openpyxl
181
+ except ImportError:
182
+ raise ImportError(
183
+ "openpyxl is required for ecoinvent Excel adapter. "
184
+ "Install it with: pip install openpyxl"
185
+ )
186
+
187
+ logger.info(f"Loading ecoinvent from {self._path.name}, sheet={self._sheet_name}")
188
+ wb = openpyxl.load_workbook(self._path, read_only=True, data_only=True)
189
+
190
+ if self._sheet_name not in wb.sheetnames:
191
+ wb.close()
192
+ raise ValueError(
193
+ f"Sheet '{self._sheet_name}' not found. "
194
+ f"Available: {wb.sheetnames}"
195
+ )
196
+
197
+ ws = wb[self._sheet_name]
198
+ headers = [cell.value for cell in next(ws.iter_rows(min_row=1, max_row=1))]
199
+ col_map = {h: i for i, h in enumerate(headers) if h}
200
+
201
+ factors = []
202
+ errors = []
203
+ total = 0
204
+ skipped = 0
205
+
206
+ for row in ws.iter_rows(min_row=2, values_only=True):
207
+ total += 1
208
+ row_list = list(row)
209
+
210
+ try:
211
+ activity_uuid = self._get_col(row_list, col_map, "Activity UUID")
212
+ activity_name = self._get_col(row_list, col_map, "Activity Name")
213
+ geography = self._get_col(row_list, col_map, "Geography")
214
+ time_period = self._get_col(row_list, col_map, "Time Period")
215
+ sector = self._get_col(row_list, col_map, "Sector")
216
+ product_name = self._get_col(row_list, col_map, "Reference Product Name")
217
+ unit = self._get_col(row_list, col_map, "Unit")
218
+ isic = self._get_col(row_list, col_map, "ISIC Classification")
219
+
220
+ if not activity_uuid or not activity_name:
221
+ skipped += 1
222
+ continue
223
+
224
+ if sector in SKIP_SECTORS:
225
+ skipped += 1
226
+ continue
227
+
228
+ if self._sectors and sector not in self._sectors:
229
+ skipped += 1
230
+ continue
231
+ if self._geographies and geography not in self._geographies:
232
+ skipped += 1
233
+ continue
234
+
235
+ source_year = self._parse_year(time_period)
236
+ applicability = product_name or activity_name
237
+ category = sector or ""
238
+ if isic:
239
+ category = f"{sector}/{isic}" if sector else isic
240
+
241
+ factor = Factor(
242
+ id=activity_uuid,
243
+ name=activity_name,
244
+ category=category,
245
+ value=0.0,
246
+ unit=unit or "",
247
+ geography={"location": geography or ""},
248
+ applicability=applicability,
249
+ source="ecoinvent 3.10",
250
+ source_year=source_year,
251
+ raw_data={
252
+ "product_name": product_name,
253
+ "isic": isic,
254
+ "time_period": time_period,
255
+ "system_model": self._sheet_name,
256
+ },
257
+ )
258
+ factors.append(factor)
259
+
260
+ except Exception as e:
261
+ errors.append(f"Row {total}: {str(e)}")
262
+
263
+ wb.close()
264
+
265
+ result = ImportResult(
266
+ total_rows=total,
267
+ imported=len(factors),
268
+ skipped=skipped,
269
+ errors=errors,
270
+ )
271
+ logger.info(f"Loaded {len(factors)} ecoinvent factors (skipped {skipped})")
272
+
273
+ return factors, result
274
+
275
+ @staticmethod
276
+ def _get_col(row: list, col_map: dict, header: str) -> str | None:
277
+ """Get a column value by header name."""
278
+ idx = col_map.get(header)
279
+ if idx is None or idx >= len(row):
280
+ return None
281
+ val = row[idx]
282
+ return str(val).strip() if val else None
283
+
284
+ @staticmethod
285
+ def _parse_year(time_period: str) -> int:
286
+ """Extract start year from time period string like '2019 - 2023'."""
287
+ if not time_period:
288
+ return 0
289
+ try:
290
+ return int(time_period[:4])
291
+ except (ValueError, IndexError):
292
+ return 0