soko-mcp 0.1.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.
- soko_mcp-0.1.0/LICENSE +3 -0
- soko_mcp-0.1.0/PKG-INFO +74 -0
- soko_mcp-0.1.0/README.md +52 -0
- soko_mcp-0.1.0/pyproject.toml +33 -0
- soko_mcp-0.1.0/setup.cfg +4 -0
- soko_mcp-0.1.0/src/soko_mcp/__init__.py +2 -0
- soko_mcp-0.1.0/src/soko_mcp/main.py +328 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/PKG-INFO +74 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/SOURCES.txt +11 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/dependency_links.txt +1 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/entry_points.txt +2 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/requires.txt +1 -0
- soko_mcp-0.1.0/src/soko_mcp.egg-info/top_level.txt +1 -0
soko_mcp-0.1.0/LICENSE
ADDED
soko_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soko-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for East Africa commodity price intelligence and market signals
|
|
5
|
+
Author-email: Gabriel Mahia <contact@aikungfu.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gabrielmahia/soko-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/gabrielmahia/soko-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/gabrielmahia/soko-mcp/issues
|
|
10
|
+
Keywords: mcp,commodity,kenya,africa,markets,prices,agriculture,soko
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# ๐ soko-mcp โ Kenya Commodity Price Intelligence MCP Server
|
|
24
|
+
|
|
25
|
+
**First commodity price intelligence MCP server for East Africa.**
|
|
26
|
+
|
|
27
|
+
*Soko* = market in Swahili.
|
|
28
|
+
|
|
29
|
+
A farmer in Nakuru doesn't know that maize prices in Nairobi are 40% higher that week. Traders know. Farmers don't. This information asymmetry is one of the most costly structural disadvantages facing smallholder farmers. soko-mcp closes it.
|
|
30
|
+
|
|
31
|
+
## The Structural Problem
|
|
32
|
+
|
|
33
|
+
In mature commodity markets, price discovery is instantaneous โ futures markets, satellite price tickers, and SMS alerts exist for every major exchange. A grain elevator in Iowa checks live CME prices before making any offer.
|
|
34
|
+
|
|
35
|
+
In Kenya, most smallholder farmers receive the price the trader offers, with no independent benchmark to compare against. The result: systematic underpricing at harvest, systematic overpricing at planting.
|
|
36
|
+
|
|
37
|
+
**Information asymmetry is a tax on the poor.**
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What it does |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `commodity_price_query` | Current price for commodity at a specific market |
|
|
44
|
+
| `regional_price_comparison` | Compare prices across all major Kenya markets |
|
|
45
|
+
| `price_trend_analysis` | 6-month history + 3-month forecast with seasonal model |
|
|
46
|
+
| `sell_hold_decision` | Optimal sell/hold timing given storage costs and price trend |
|
|
47
|
+
| `market_overview` | Multi-commodity price snapshot for a market |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install soko-mcp # coming soon to PyPI
|
|
53
|
+
soko-mcp # starts on stdio
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example Queries for Claude
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
"What is the current Nairobi maize price?"
|
|
60
|
+
"Should I sell my 50 bags of beans now or wait 2 months?"
|
|
61
|
+
"Compare potato prices across all Kenya markets"
|
|
62
|
+
"Give me a price trend for avocados in Nakuru for the next 3 months"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Research Basis
|
|
66
|
+
|
|
67
|
+
- EAGC East Africa Regional Market Monitor
|
|
68
|
+
- World Bank "Information and Communication Technology and Agricultural Markets" (2016)
|
|
69
|
+
- Suri & Jack "Mobile Phones and Agricultural Performance" (2016)
|
|
70
|
+
|
|
71
|
+
โ ๏ธ DEMO data โ synthetic seasonal model. Verify at eagc.org, kalro.org, or local market boards.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
*ยฉ 2026 Gabriel Mahia / AI Kung Fu LLC ยท MIT License*
|
soko_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# ๐ soko-mcp โ Kenya Commodity Price Intelligence MCP Server
|
|
2
|
+
|
|
3
|
+
**First commodity price intelligence MCP server for East Africa.**
|
|
4
|
+
|
|
5
|
+
*Soko* = market in Swahili.
|
|
6
|
+
|
|
7
|
+
A farmer in Nakuru doesn't know that maize prices in Nairobi are 40% higher that week. Traders know. Farmers don't. This information asymmetry is one of the most costly structural disadvantages facing smallholder farmers. soko-mcp closes it.
|
|
8
|
+
|
|
9
|
+
## The Structural Problem
|
|
10
|
+
|
|
11
|
+
In mature commodity markets, price discovery is instantaneous โ futures markets, satellite price tickers, and SMS alerts exist for every major exchange. A grain elevator in Iowa checks live CME prices before making any offer.
|
|
12
|
+
|
|
13
|
+
In Kenya, most smallholder farmers receive the price the trader offers, with no independent benchmark to compare against. The result: systematic underpricing at harvest, systematic overpricing at planting.
|
|
14
|
+
|
|
15
|
+
**Information asymmetry is a tax on the poor.**
|
|
16
|
+
|
|
17
|
+
## Tools
|
|
18
|
+
|
|
19
|
+
| Tool | What it does |
|
|
20
|
+
|------|-------------|
|
|
21
|
+
| `commodity_price_query` | Current price for commodity at a specific market |
|
|
22
|
+
| `regional_price_comparison` | Compare prices across all major Kenya markets |
|
|
23
|
+
| `price_trend_analysis` | 6-month history + 3-month forecast with seasonal model |
|
|
24
|
+
| `sell_hold_decision` | Optimal sell/hold timing given storage costs and price trend |
|
|
25
|
+
| `market_overview` | Multi-commodity price snapshot for a market |
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install soko-mcp # coming soon to PyPI
|
|
31
|
+
soko-mcp # starts on stdio
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example Queries for Claude
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
"What is the current Nairobi maize price?"
|
|
38
|
+
"Should I sell my 50 bags of beans now or wait 2 months?"
|
|
39
|
+
"Compare potato prices across all Kenya markets"
|
|
40
|
+
"Give me a price trend for avocados in Nakuru for the next 3 months"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Research Basis
|
|
44
|
+
|
|
45
|
+
- EAGC East Africa Regional Market Monitor
|
|
46
|
+
- World Bank "Information and Communication Technology and Agricultural Markets" (2016)
|
|
47
|
+
- Suri & Jack "Mobile Phones and Agricultural Performance" (2016)
|
|
48
|
+
|
|
49
|
+
โ ๏ธ DEMO data โ synthetic seasonal model. Verify at eagc.org, kalro.org, or local market boards.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
*ยฉ 2026 Gabriel Mahia / AI Kung Fu LLC ยท MIT License*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "soko-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for East Africa commodity price intelligence and market signals"
|
|
9
|
+
authors = [{name = "Gabriel Mahia", email = "contact@aikungfu.dev"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["mcp", "commodity", "kenya", "africa", "markets", "prices", "agriculture", "soko"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Topic :: Office/Business :: Financial",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["fastmcp>=2.0.0"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/gabrielmahia/soko-mcp"
|
|
26
|
+
Repository = "https://github.com/gabrielmahia/soko-mcp"
|
|
27
|
+
Issues = "https://github.com/gabrielmahia/soko-mcp/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
soko-mcp = "soko_mcp.main:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
soko_mcp-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# soko-mcp โ Commodity Price Intelligence MCP Server
|
|
3
|
+
# ยฉ 2026 Gabriel Mahia / AI Kung Fu LLC โ MIT License
|
|
4
|
+
#
|
|
5
|
+
# The problem: Kenya smallholder farmers make sell/hold decisions with zero price data.
|
|
6
|
+
# A maize farmer in Nakuru doesn't know that Nairobi prices are 40% higher that week.
|
|
7
|
+
# The information asymmetry flows upward โ traders know, farmers don't.
|
|
8
|
+
# Western parallel: CME Group commodity futures, USDA NASS reports, Reuters Commodities
|
|
9
|
+
# African context: EAGC (East Africa Grain Council) collects data but it's hard to access
|
|
10
|
+
# Research: "Information and Communication Technology and Agricultural Markets" (World Bank, 2016)
|
|
11
|
+
# "Mobile Phones and Agricultural Performance" (Suri & Jack, 2016)
|
|
12
|
+
#
|
|
13
|
+
# TRUST INTEGRITY: All prices are DEMO/synthetic. Verify at EAGC, KALRO, or county markets.
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import datetime
|
|
18
|
+
import random
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
from fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
mcp = FastMCP(
|
|
23
|
+
name="soko-mcp",
|
|
24
|
+
instructions="""Kenya commodity price intelligence MCP server.
|
|
25
|
+
Provides tools for market price queries, regional comparisons, trend analysis,
|
|
26
|
+
and sell/hold decision support for farmers and traders.
|
|
27
|
+
|
|
28
|
+
IMPORTANT: All prices are DEMO/synthetic for educational purposes.
|
|
29
|
+
Real prices: EAGC (eagc.org), KALRO (kalro.org), county market boards.
|
|
30
|
+
Not affiliated with any commodity exchange.
|
|
31
|
+
""",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
TODAY = datetime.date.today()
|
|
35
|
+
MONTH = TODAY.month
|
|
36
|
+
|
|
37
|
+
# DEMO commodity price database (KES per 90kg bag unless noted)
|
|
38
|
+
# Seasonal patterns built in. All DEMO โ verify at eagc.org, kalro.org
|
|
39
|
+
_BASE_PRICES = {
|
|
40
|
+
"maize": {"nairobi": 3200, "nakuru": 2800, "eldoret": 2600, "kisumu": 3000, "mombasa": 3500, "unit": "90kg bag"},
|
|
41
|
+
"beans": {"nairobi": 12000, "nakuru": 10500, "eldoret": 10000, "kisumu": 11000, "mombasa": 12500, "unit": "90kg bag"},
|
|
42
|
+
"wheat": {"nairobi": 4500, "nakuru": 4200, "eldoret": 4000, "kisumu": 4600, "mombasa": 4800, "unit": "90kg bag"},
|
|
43
|
+
"potatoes": {"nairobi": 2800, "nakuru": 1800, "eldoret": 1600, "kisumu": 2500, "mombasa": 3000, "unit": "50kg bag"},
|
|
44
|
+
"tomatoes": {"nairobi": 4000, "nakuru": 2500, "eldoret": 2200, "kisumu": 3500, "mombasa": 4500, "unit": "crate (64kg)"},
|
|
45
|
+
"avocados": {"nairobi": 8000, "nakuru": 6000, "eldoret": 5500, "kisumu": 7000, "mombasa": 9000, "unit": "bag (60 pieces)"},
|
|
46
|
+
"tea_bulk": {"nairobi": 200, "nakuru": 180, "eldoret": 170, "kisumu": 210, "mombasa": 220, "unit": "kg"},
|
|
47
|
+
"coffee_parchment": {"nairobi": 120, "nakuru": 100, "eldoret": 95, "kisumu": 110, "mombasa": 125, "unit": "kg"},
|
|
48
|
+
"sugarcane": {"nairobi": 0, "nakuru": 4200, "eldoret": 3800, "kisumu": 4500, "mombasa": 0, "unit": "tonne"},
|
|
49
|
+
"onions": {"nairobi": 6000, "nakuru": 4500, "eldoret": 4000, "kisumu": 5500, "mombasa": 6500, "unit": "50kg bag"},
|
|
50
|
+
"kale_sukuma": {"nairobi": 30, "nakuru": 20, "eldoret": 18, "kisumu": 25, "mombasa": 35, "unit": "bunch"},
|
|
51
|
+
"milk": {"nairobi": 60, "nakuru": 45, "eldoret": 40, "kisumu": 50, "mombasa": 65, "unit": "litre"},
|
|
52
|
+
"chicken": {"nairobi": 600, "nakuru": 500, "eldoret": 450, "kisumu": 550, "mombasa": 650, "unit": "kg live"},
|
|
53
|
+
"eggs": {"nairobi": 15, "nakuru": 13, "eldoret": 12, "kisumu": 14, "mombasa": 16, "unit": "piece"},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Seasonal multipliers (month index 1-12)
|
|
57
|
+
# Long rains harvest (June-July): +supply = lower prices
|
|
58
|
+
# Short rains harvest (Dec-Jan): +supply = lower prices
|
|
59
|
+
# Lean seasons (Feb-May, Aug-Nov): higher prices
|
|
60
|
+
_SEASONAL = {
|
|
61
|
+
1: 0.90, 2: 1.10, 3: 1.15, 4: 1.10, 5: 1.05,
|
|
62
|
+
6: 0.85, 7: 0.80, 8: 1.00, 9: 1.05, 10: 1.10,
|
|
63
|
+
11: 1.05, 12: 0.90
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def _price(commodity, market):
|
|
67
|
+
base = _BASE_PRICES.get(commodity, {}).get(market.lower())
|
|
68
|
+
if not base: return None
|
|
69
|
+
return int(base * _SEASONAL.get(MONTH, 1.0))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@mcp.tool(
|
|
73
|
+
description=(
|
|
74
|
+
"Query current commodity prices at Kenya markets. "
|
|
75
|
+
"Western parallel: CME Group spot price query, USDA Agricultural Marketing Service. "
|
|
76
|
+
"DEMO prices โ verify at eagc.org or county market boards."
|
|
77
|
+
),
|
|
78
|
+
annotations={"readOnlyHint": True},
|
|
79
|
+
)
|
|
80
|
+
def commodity_price_query(
|
|
81
|
+
commodity: Annotated[str, "Commodity: maize, beans, wheat, potatoes, tomatoes, avocados, tea_bulk, coffee_parchment, sugarcane, onions, kale_sukuma, milk, chicken, eggs"],
|
|
82
|
+
market: Annotated[str, "Market: nairobi, nakuru, eldoret, kisumu, mombasa"],
|
|
83
|
+
) -> dict:
|
|
84
|
+
price = _price(commodity.lower(), market.lower())
|
|
85
|
+
if price is None:
|
|
86
|
+
available = list(_BASE_PRICES.get(commodity.lower(), {}).keys())
|
|
87
|
+
return {"status": "NOT_FOUND",
|
|
88
|
+
"message": f"No data for {commodity} in {market}",
|
|
89
|
+
"markets_with_data": available}
|
|
90
|
+
|
|
91
|
+
info = _BASE_PRICES[commodity.lower()]
|
|
92
|
+
season = ("HARVEST (supply high, prices lower)" if MONTH in (6,7,12,1)
|
|
93
|
+
else "LEAN (supply lower, prices higher)" if MONTH in (2,3,4,5,8,9,10,11)
|
|
94
|
+
else "MID-SEASON")
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"status": "OK",
|
|
98
|
+
"commodity": commodity,
|
|
99
|
+
"market": market,
|
|
100
|
+
"price_kes": price,
|
|
101
|
+
"unit": info["unit"],
|
|
102
|
+
"date": TODAY.isoformat(),
|
|
103
|
+
"season": season,
|
|
104
|
+
"note": "DEMO โ Synthetic price. Verify at eagc.org, KALRO, or the local market board.",
|
|
105
|
+
"source": "soko-mcp DEMO dataset. Reference: EAGC market data format.",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool(
|
|
110
|
+
description=(
|
|
111
|
+
"Compare commodity prices across all Kenya markets to find the best price. "
|
|
112
|
+
"Western parallel: CME regional basis reports, DTN ProphetX. "
|
|
113
|
+
"This is the 'should I take my maize to Nairobi or sell locally?' tool. "
|
|
114
|
+
"DEMO prices."
|
|
115
|
+
),
|
|
116
|
+
annotations={"readOnlyHint": True},
|
|
117
|
+
)
|
|
118
|
+
def regional_price_comparison(
|
|
119
|
+
commodity: Annotated[str, "Commodity to compare (maize, beans, wheat, potatoes, avocados, etc.)"],
|
|
120
|
+
farmer_location: Annotated[str, "Farmer's location/county (for transport cost context)"],
|
|
121
|
+
) -> dict:
|
|
122
|
+
markets = ["nairobi", "nakuru", "eldoret", "kisumu", "mombasa"]
|
|
123
|
+
prices = {}
|
|
124
|
+
for m in markets:
|
|
125
|
+
p = _price(commodity.lower(), m)
|
|
126
|
+
if p: prices[m] = p
|
|
127
|
+
|
|
128
|
+
if not prices:
|
|
129
|
+
return {"status": "NOT_FOUND", "message": f"No regional data for {commodity}"}
|
|
130
|
+
|
|
131
|
+
sorted_markets = sorted(prices.items(), key=lambda x: x[1], reverse=True)
|
|
132
|
+
best_market, best_price = sorted_markets[0]
|
|
133
|
+
worst_market, worst_price = sorted_markets[-1]
|
|
134
|
+
spread_kes = best_price - worst_price
|
|
135
|
+
spread_pct = round(spread_kes / worst_price * 100, 1)
|
|
136
|
+
|
|
137
|
+
unit = _BASE_PRICES.get(commodity.lower(), {}).get("unit", "unit")
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"status": "OK",
|
|
141
|
+
"commodity": commodity,
|
|
142
|
+
"comparison_date": TODAY.isoformat(),
|
|
143
|
+
"prices_by_market": {m: {"price_kes": p, "unit": unit} for m, p in sorted_markets},
|
|
144
|
+
"best_market": {"market": best_market, "price_kes": best_price},
|
|
145
|
+
"worst_market": {"market": worst_market, "price_kes": worst_price},
|
|
146
|
+
"price_spread": {"kes": spread_kes, "percent": f"{spread_pct}%"},
|
|
147
|
+
"transport_note": (
|
|
148
|
+
f"Price spread of KES {spread_kes:,} across markets. "
|
|
149
|
+
f"Transport from {farmer_location} to {best_market}: estimate KES 2,000-8,000/tonne "
|
|
150
|
+
f"(actual: check county cooperatives or boda boda aggregators). "
|
|
151
|
+
f"Net benefit calculation required before deciding to transport."
|
|
152
|
+
),
|
|
153
|
+
"arbitrage_opportunity": spread_pct > 15,
|
|
154
|
+
"note": "DEMO โ Synthetic prices. Verify at eagc.org before making transport decisions.",
|
|
155
|
+
"source": "soko-mcp DEMO dataset.",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@mcp.tool(
|
|
160
|
+
description=(
|
|
161
|
+
"Analyse price trends for a commodity to inform sell/hold timing. "
|
|
162
|
+
"Western parallel: CME seasonal charts, DTN market commentary. "
|
|
163
|
+
"DEMO historical simulation."
|
|
164
|
+
),
|
|
165
|
+
annotations={"readOnlyHint": True},
|
|
166
|
+
)
|
|
167
|
+
def price_trend_analysis(
|
|
168
|
+
commodity: Annotated[str, "Commodity to analyse"],
|
|
169
|
+
market: Annotated[str, "Market: nairobi, nakuru, eldoret, kisumu, mombasa"],
|
|
170
|
+
months_ahead: Annotated[int, "Forecast horizon in months (1-6)"] = 3,
|
|
171
|
+
) -> dict:
|
|
172
|
+
base = _BASE_PRICES.get(commodity.lower(), {}).get(market.lower())
|
|
173
|
+
if not base:
|
|
174
|
+
return {"status": "NOT_FOUND", "message": f"No data for {commodity} in {market}"}
|
|
175
|
+
|
|
176
|
+
# Build 6-month history and 3-month forecast using seasonal model
|
|
177
|
+
history = []
|
|
178
|
+
for i in range(6, 0, -1):
|
|
179
|
+
m = ((MONTH - i - 1) % 12) + 1
|
|
180
|
+
hist_price = int(base * _SEASONAL.get(m, 1.0))
|
|
181
|
+
history.append({
|
|
182
|
+
"month": (TODAY.replace(day=1) - datetime.timedelta(days=30*i)).strftime("%Y-%m"),
|
|
183
|
+
"price_kes": hist_price
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
forecast = []
|
|
187
|
+
for i in range(1, months_ahead + 1):
|
|
188
|
+
m = ((MONTH + i - 1) % 12) + 1
|
|
189
|
+
fcast_price = int(base * _SEASONAL.get(m, 1.0))
|
|
190
|
+
forecast.append({
|
|
191
|
+
"month": (TODAY.replace(day=1) + datetime.timedelta(days=30*i)).strftime("%Y-%m"),
|
|
192
|
+
"price_kes": fcast_price,
|
|
193
|
+
"confidence": "LOW โ demo seasonal model only"
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
current_price = _price(commodity.lower(), market.lower())
|
|
197
|
+
next_month_price = forecast[0]["price_kes"] if forecast else current_price
|
|
198
|
+
trend = "RISING" if next_month_price > current_price * 1.03 else \
|
|
199
|
+
"FALLING" if next_month_price < current_price * 0.97 else "STABLE"
|
|
200
|
+
|
|
201
|
+
unit = _BASE_PRICES.get(commodity.lower(), {}).get("unit", "unit")
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"status": "OK",
|
|
205
|
+
"commodity": commodity,
|
|
206
|
+
"market": market,
|
|
207
|
+
"current_price_kes": current_price,
|
|
208
|
+
"unit": unit,
|
|
209
|
+
"price_trend": trend,
|
|
210
|
+
"6_month_history": history,
|
|
211
|
+
f"{months_ahead}_month_forecast": forecast,
|
|
212
|
+
"trend_driver": {
|
|
213
|
+
"RISING": "Post-harvest supplies decreasing. Lean season approaching. Prices typically rise.",
|
|
214
|
+
"FALLING": "Harvest season approaching. Increased supply expected. Prices typically fall.",
|
|
215
|
+
"STABLE": "Mid-season. No major supply/demand shift expected in near term.",
|
|
216
|
+
}.get(trend, ""),
|
|
217
|
+
"warning": "DEMO โ Seasonal model only. Real prices affected by weather, exports, government policy, currency. Verify at eagc.org.",
|
|
218
|
+
"source": "soko-mcp DEMO seasonal model. Reference: EAGC East Africa Regional Market Monitor.",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@mcp.tool(
|
|
223
|
+
description=(
|
|
224
|
+
"Give a sell/hold recommendation for a commodity based on price trends. "
|
|
225
|
+
"Western parallel: CME basis trading decision tools, grain elevator advisory. "
|
|
226
|
+
"This is the 'what should I do with my harvest today?' AI advisor. "
|
|
227
|
+
"DEMO model โ always verify prices before acting."
|
|
228
|
+
),
|
|
229
|
+
annotations={"readOnlyHint": True},
|
|
230
|
+
)
|
|
231
|
+
def sell_hold_decision(
|
|
232
|
+
commodity: Annotated[str, "Your commodity"],
|
|
233
|
+
market: Annotated[str, "Your nearest market"],
|
|
234
|
+
quantity_bags: Annotated[int, "Quantity you want to sell (bags or units)"],
|
|
235
|
+
storage_cost_per_month_kes: Annotated[int, "Your storage cost per month in KES (0 if storing is free)"] = 0,
|
|
236
|
+
months_can_store: Annotated[int, "Maximum months you can store the commodity"] = 3,
|
|
237
|
+
) -> dict:
|
|
238
|
+
current = _price(commodity.lower(), market.lower())
|
|
239
|
+
if not current:
|
|
240
|
+
return {"status": "NOT_FOUND", "message": f"No data for {commodity} in {market}"}
|
|
241
|
+
|
|
242
|
+
unit = _BASE_PRICES.get(commodity.lower(), {}).get("unit", "unit")
|
|
243
|
+
total_current_value = current * quantity_bags
|
|
244
|
+
storage_total = storage_cost_per_month_kes * months_can_store
|
|
245
|
+
|
|
246
|
+
# Project future prices
|
|
247
|
+
future_prices = []
|
|
248
|
+
for i in range(1, months_can_store + 1):
|
|
249
|
+
m = ((MONTH + i - 1) % 12) + 1
|
|
250
|
+
base = _BASE_PRICES[commodity.lower()][market.lower()]
|
|
251
|
+
fp = int(base * _SEASONAL.get(m, 1.0))
|
|
252
|
+
net_gain = (fp - current) * quantity_bags - (storage_cost_per_month_kes * i)
|
|
253
|
+
future_prices.append({
|
|
254
|
+
"months_from_now": i,
|
|
255
|
+
"forecast_price_kes": fp,
|
|
256
|
+
"projected_value_kes": fp * quantity_bags,
|
|
257
|
+
"storage_cost_kes": storage_cost_per_month_kes * i,
|
|
258
|
+
"net_gain_vs_selling_now_kes": net_gain,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
best_option = max(future_prices, key=lambda x: x["net_gain_vs_selling_now_kes"])
|
|
262
|
+
sell_now_better = best_option["net_gain_vs_selling_now_kes"] <= 0
|
|
263
|
+
|
|
264
|
+
recommendation = "SELL NOW" if sell_now_better else f"HOLD {best_option['months_from_now']} MONTHS"
|
|
265
|
+
rationale = (
|
|
266
|
+
f"Selling now yields KES {total_current_value:,}. "
|
|
267
|
+
+ (f"Expected price rise in {best_option['months_from_now']} months would add KES {best_option['net_gain_vs_selling_now_kes']:,} after storage costs."
|
|
268
|
+
if not sell_now_better else
|
|
269
|
+
"Storage costs exceed the expected price increase. Sell now.")
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"status": "OK",
|
|
274
|
+
"commodity": commodity,
|
|
275
|
+
"market": market,
|
|
276
|
+
"quantity": f"{quantity_bags} {unit}s",
|
|
277
|
+
"current_value_kes": total_current_value,
|
|
278
|
+
"recommendation": recommendation,
|
|
279
|
+
"rationale": rationale,
|
|
280
|
+
"scenarios": future_prices,
|
|
281
|
+
"caveat": "DEMO model โ based on seasonal averages only. Real decisions require checking actual market prices, storage quality, buyer contracts, and weather forecasts.",
|
|
282
|
+
"verify_prices_at": ["eagc.org", "kalro.org", f"County Market Coordinator โ {market} area"],
|
|
283
|
+
"source": "soko-mcp DEMO seasonal model.",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@mcp.tool(
|
|
288
|
+
description=(
|
|
289
|
+
"Query commodity prices for multiple markets and commodities in one call. "
|
|
290
|
+
"Useful for traders comparing portfolios across regions. "
|
|
291
|
+
"DEMO data."
|
|
292
|
+
),
|
|
293
|
+
annotations={"readOnlyHint": True},
|
|
294
|
+
)
|
|
295
|
+
def market_overview(
|
|
296
|
+
commodities: Annotated[str, "Comma-separated list of commodities (e.g., 'maize,beans,potatoes')"],
|
|
297
|
+
market: Annotated[str, "Primary market to query"] = "nairobi",
|
|
298
|
+
) -> dict:
|
|
299
|
+
commodity_list = [c.strip().lower() for c in commodities.split(",")]
|
|
300
|
+
results = {}
|
|
301
|
+
for c in commodity_list:
|
|
302
|
+
p = _price(c, market.lower())
|
|
303
|
+
if p:
|
|
304
|
+
unit = _BASE_PRICES.get(c, {}).get("unit", "?")
|
|
305
|
+
results[c] = {"price_kes": p, "unit": unit}
|
|
306
|
+
|
|
307
|
+
season_note = {
|
|
308
|
+
True: "HARVEST SEASON โ supplies high, prices typically below annual average",
|
|
309
|
+
False: "LEAN SEASON โ supplies lower, prices typically above annual average",
|
|
310
|
+
}[MONTH in (6, 7, 12, 1)]
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"status": "OK",
|
|
314
|
+
"market": market,
|
|
315
|
+
"date": TODAY.isoformat(),
|
|
316
|
+
"season_context": season_note,
|
|
317
|
+
"prices": results,
|
|
318
|
+
"commodities_not_found": [c for c in commodity_list if c not in results],
|
|
319
|
+
"note": "DEMO โ Synthetic prices. Verify at eagc.org or county market boards.",
|
|
320
|
+
"source": "soko-mcp DEMO dataset.",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def main():
|
|
325
|
+
mcp.run()
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soko-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for East Africa commodity price intelligence and market signals
|
|
5
|
+
Author-email: Gabriel Mahia <contact@aikungfu.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gabrielmahia/soko-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/gabrielmahia/soko-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/gabrielmahia/soko-mcp/issues
|
|
10
|
+
Keywords: mcp,commodity,kenya,africa,markets,prices,agriculture,soko
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# ๐ soko-mcp โ Kenya Commodity Price Intelligence MCP Server
|
|
24
|
+
|
|
25
|
+
**First commodity price intelligence MCP server for East Africa.**
|
|
26
|
+
|
|
27
|
+
*Soko* = market in Swahili.
|
|
28
|
+
|
|
29
|
+
A farmer in Nakuru doesn't know that maize prices in Nairobi are 40% higher that week. Traders know. Farmers don't. This information asymmetry is one of the most costly structural disadvantages facing smallholder farmers. soko-mcp closes it.
|
|
30
|
+
|
|
31
|
+
## The Structural Problem
|
|
32
|
+
|
|
33
|
+
In mature commodity markets, price discovery is instantaneous โ futures markets, satellite price tickers, and SMS alerts exist for every major exchange. A grain elevator in Iowa checks live CME prices before making any offer.
|
|
34
|
+
|
|
35
|
+
In Kenya, most smallholder farmers receive the price the trader offers, with no independent benchmark to compare against. The result: systematic underpricing at harvest, systematic overpricing at planting.
|
|
36
|
+
|
|
37
|
+
**Information asymmetry is a tax on the poor.**
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What it does |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `commodity_price_query` | Current price for commodity at a specific market |
|
|
44
|
+
| `regional_price_comparison` | Compare prices across all major Kenya markets |
|
|
45
|
+
| `price_trend_analysis` | 6-month history + 3-month forecast with seasonal model |
|
|
46
|
+
| `sell_hold_decision` | Optimal sell/hold timing given storage costs and price trend |
|
|
47
|
+
| `market_overview` | Multi-commodity price snapshot for a market |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install soko-mcp # coming soon to PyPI
|
|
53
|
+
soko-mcp # starts on stdio
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example Queries for Claude
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
"What is the current Nairobi maize price?"
|
|
60
|
+
"Should I sell my 50 bags of beans now or wait 2 months?"
|
|
61
|
+
"Compare potato prices across all Kenya markets"
|
|
62
|
+
"Give me a price trend for avocados in Nakuru for the next 3 months"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Research Basis
|
|
66
|
+
|
|
67
|
+
- EAGC East Africa Regional Market Monitor
|
|
68
|
+
- World Bank "Information and Communication Technology and Agricultural Markets" (2016)
|
|
69
|
+
- Suri & Jack "Mobile Phones and Agricultural Performance" (2016)
|
|
70
|
+
|
|
71
|
+
โ ๏ธ DEMO data โ synthetic seasonal model. Verify at eagc.org, kalro.org, or local market boards.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
*ยฉ 2026 Gabriel Mahia / AI Kung Fu LLC ยท MIT License*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/soko_mcp/__init__.py
|
|
5
|
+
src/soko_mcp/main.py
|
|
6
|
+
src/soko_mcp.egg-info/PKG-INFO
|
|
7
|
+
src/soko_mcp.egg-info/SOURCES.txt
|
|
8
|
+
src/soko_mcp.egg-info/dependency_links.txt
|
|
9
|
+
src/soko_mcp.egg-info/entry_points.txt
|
|
10
|
+
src/soko_mcp.egg-info/requires.txt
|
|
11
|
+
src/soko_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastmcp>=2.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
soko_mcp
|