gildea 0.1.0__py3-none-any.whl

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,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: gildea
3
+ Version: 0.1.0
4
+ Summary: Python client and MCP server for the Gildea AI market intelligence API
5
+ Project-URL: Homepage, https://gildea.ai
6
+ Project-URL: Documentation, https://docs.gildea.ai
7
+ Project-URL: Repository, https://github.com/hjones20/gildea-api
8
+ Project-URL: Issues, https://github.com/hjones20/gildea-api/issues
9
+ Author: Holly Jones
10
+ License-Expression: MIT
11
+ Keywords: ai,api-client,competitive-intelligence,market-intelligence,mcp
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.27
23
+ Provides-Extra: dev
24
+ Requires-Dist: mcp[server]>=1.3; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
26
+ Requires-Dist: pytest-httpx>=0.35; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: ruff>=0.15; extra == 'dev'
29
+ Provides-Extra: mcp
30
+ Requires-Dist: mcp[server]>=1.3; extra == 'mcp'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Gildea
34
+
35
+ Python client and MCP server for the [Gildea](https://gildea.ai) AI market intelligence API.
36
+
37
+ Gildea tracks 500+ expert sources on AI, decomposes every signal into verified reasoning chains (thesis, arguments, claims, evidence), and serves it through a REST API. This package gives you a Python client and an MCP server so AI assistants can use the data directly.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ # Python client only
43
+ pip install gildea
44
+
45
+ # With MCP server
46
+ pip install gildea[mcp]
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ from gildea_sdk import Gildea
53
+
54
+ client = Gildea(api_key="gld_your_key_here")
55
+
56
+ # Search verified text units
57
+ results = client.search.query(q="data center infrastructure spending")
58
+ for hit in results.data:
59
+ print(f"{hit.unit.text}")
60
+ print(f" Source: {hit.citation.signal_title} ({hit.citation.registrable_domain})")
61
+
62
+ # Get full signal decomposition with evidence
63
+ signal = client.signals.get("signal_id", include="evidence")
64
+
65
+ # Entity intelligence with trend analytics
66
+ entity = client.entities.get("NVIDIA")
67
+ print(f"{entity.display_name}: {entity.direction}, {entity.scale} scale, {entity.priority} priority")
68
+
69
+ # Cross-source consensus mapping
70
+ similar = client.search.query(similar_to="unit_id")
71
+ ```
72
+
73
+ ## MCP Server
74
+
75
+ Use Gildea as a tool in Claude, ChatGPT, Cursor, VS Code, or any MCP-compatible client.
76
+
77
+ ```bash
78
+ # Run directly
79
+ gildea-mcp
80
+
81
+ # Or via uvx (no install needed)
82
+ uvx --from gildea[mcp] gildea-mcp
83
+ ```
84
+
85
+ Set your API key:
86
+ ```bash
87
+ export GILDEA_API_KEY=gld_your_key_here
88
+ ```
89
+
90
+ ### Claude Desktop
91
+
92
+ Add to your `claude_desktop_config.json`:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "gildea": {
98
+ "command": "uvx",
99
+ "args": ["--from", "gildea[mcp]", "gildea-mcp"],
100
+ "env": {
101
+ "GILDEA_API_KEY": "gld_your_key_here"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Available MCP Tools
109
+
110
+ | Tool | Description |
111
+ |------|-------------|
112
+ | `search_text_units` | Semantic search across verified text units, or vector similarity via `similar_to` |
113
+ | `list_signals` | Browse signals by entity, theme, date, content type |
114
+ | `get_signal_detail` | Full decomposition: thesis, arguments, claims, evidence |
115
+ | `get_entity_profile` | Entity trend analytics, co-occurrence, theme distribution |
116
+ | `list_entities` | Discover entities by trend direction, priority, scale |
117
+ | `get_themes` | Theme overview across value chain and market force axes |
118
+ | `get_theme_detail` | Single theme trend analytics and cross-theme relationships |
119
+
120
+ ## API Key
121
+
122
+ Get your API key at [gildea.ai](https://gildea.ai). Free tier includes 5 requests/minute and 200 requests/month.
123
+
124
+ ## Documentation
125
+
126
+ Full API docs at [docs.gildea.ai](https://docs.gildea.ai).
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,17 @@
1
+ gildea_sdk/__init__.py,sha256=53NUhdCq50QD1meHc_ge5HuLycGkGLM_nQLJ2EUzpAw,407
2
+ gildea_sdk/client.py,sha256=ZD8DkgFNP1qfOdRSU1qUoY-tKVWnhXCH2n_D5ccxNfI,1571
3
+ gildea_sdk/exceptions.py,sha256=NOMrpRwSEn1iBbavmmz0vKBSJl_B8Mv9lEXxaCx6-Qw,857
4
+ gildea_sdk/pagination.py,sha256=h3-swBnUhKm8MRr6cm7SEMJ6shkga14PYZbuETukqn0,2368
5
+ gildea_sdk/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ gildea_sdk/mcp/__main__.py,sha256=WMQP7_MpK6gabqrvB1g5Is78a2rAmazjOC9DXG6BBgg,108
7
+ gildea_sdk/mcp/server.py,sha256=AWunr6kPXUWrJendrJ7EGuEYPx_wZ-utdstFxX5Q5Tc,16232
8
+ gildea_sdk/mcp/tools.py,sha256=7DyQWnoytNqCYJw0TBpP_J1UGDZHLUUGWk--CioWNfE,4447
9
+ gildea_sdk/resources/__init__.py,sha256=P27eKn1HiLXW_DC3LWPH915RE3fmncBMzHv7SC6vubw,233
10
+ gildea_sdk/resources/entities.py,sha256=2iigKaVY920ij9Ss8vWXBTds6ehN1IfvjXhMK7E3Igo,1353
11
+ gildea_sdk/resources/search.py,sha256=rpqlUMvE-9popFSoLjyUKpsjOPk6jHKHiAB1lkLhB_A,1254
12
+ gildea_sdk/resources/signals.py,sha256=bEbz3_xPnziolcFOmR83qzAxjD5R0Yee02x2X9GQ7So,1371
13
+ gildea_sdk/resources/themes.py,sha256=HWSmgfzBqdpltSbD4vqZTgAto0peUmg0D2l4MV3Nd8Y,489
14
+ gildea-0.1.0.dist-info/METADATA,sha256=Linn2AV_LnO8rLMMcgV_Yli26gPrZFC2jl9nBN-PSOg,3984
15
+ gildea-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ gildea-0.1.0.dist-info/entry_points.txt,sha256=QiEXWZnOXrTFA63upviecr_w_cOMfwzYUOjSWxgNaZw,58
17
+ gildea-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gildea-mcp = gildea_sdk.mcp.server:main
gildea_sdk/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """Gildea Python SDK — client for the Gildea AI market intelligence API."""
2
+
3
+ from .client import Gildea
4
+ from .exceptions import (
5
+ APIError,
6
+ AuthenticationError,
7
+ BadRequestError,
8
+ GildeaError,
9
+ NotFoundError,
10
+ RateLimitError,
11
+ )
12
+
13
+ __all__ = [
14
+ "Gildea",
15
+ "GildeaError",
16
+ "AuthenticationError",
17
+ "NotFoundError",
18
+ "RateLimitError",
19
+ "BadRequestError",
20
+ "APIError",
21
+ ]
gildea_sdk/client.py ADDED
@@ -0,0 +1,49 @@
1
+ """Main Gildea client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import httpx
8
+
9
+ from .exceptions import AuthenticationError
10
+ from .resources import EntitiesResource, SearchResource, SignalsResource, ThemesResource
11
+
12
+
13
+ class Gildea:
14
+ """Synchronous client for the Gildea AI market intelligence API."""
15
+
16
+ def __init__(self, api_key=None, base_url="https://api.gildea.ai", timeout=30.0):
17
+ self.api_key = api_key or os.environ.get("GILDEA_API_KEY")
18
+ if not self.api_key:
19
+ raise AuthenticationError(
20
+ "No API key provided. Set GILDEA_API_KEY or pass api_key=."
21
+ )
22
+ self._client = httpx.Client(
23
+ base_url=base_url,
24
+ headers={"X-API-Key": self.api_key},
25
+ timeout=timeout,
26
+ )
27
+ self.signals = SignalsResource(self._client)
28
+ self.entities = EntitiesResource(self._client)
29
+ self.themes = ThemesResource(self._client)
30
+ self._search = SearchResource(self._client)
31
+
32
+ def search(self, query=None, *, similar_to=None, **kwargs):
33
+ """Search across all verified text units.
34
+
35
+ Two modes: pass ``query`` for hybrid semantic + keyword search,
36
+ or ``similar_to`` with a text unit ID for pure vector similarity.
37
+ Exactly one is required.
38
+ """
39
+ return self._search.query(query, similar_to=similar_to, **kwargs)
40
+
41
+ def close(self):
42
+ """Close the underlying HTTP connection."""
43
+ self._client.close()
44
+
45
+ def __enter__(self):
46
+ return self
47
+
48
+ def __exit__(self, *args):
49
+ self.close()
@@ -0,0 +1,34 @@
1
+ """Exception classes for the Gildea SDK."""
2
+
3
+
4
+ class GildeaError(Exception):
5
+ """Base exception for all Gildea SDK errors."""
6
+
7
+ def __init__(self, message, *, code=None, status=None):
8
+ super().__init__(message)
9
+ self.code = code
10
+ self.status = status
11
+
12
+
13
+ class AuthenticationError(GildeaError):
14
+ """Raised on 401/403 responses or missing API key."""
15
+
16
+
17
+ class NotFoundError(GildeaError):
18
+ """Raised on 404 responses."""
19
+
20
+
21
+ class RateLimitError(GildeaError):
22
+ """Raised on 429 responses."""
23
+
24
+ def __init__(self, message, *, code=None, status=None, retry_after=None):
25
+ super().__init__(message, code=code, status=status)
26
+ self.retry_after = retry_after
27
+
28
+
29
+ class BadRequestError(GildeaError):
30
+ """Raised on 400 responses."""
31
+
32
+
33
+ class APIError(GildeaError):
34
+ """Raised on 5xx or other unexpected responses."""
File without changes
@@ -0,0 +1,5 @@
1
+ """Allow running MCP server as: python -m gildea_sdk.mcp"""
2
+
3
+ from gildea_sdk.mcp.server import main
4
+
5
+ main()
@@ -0,0 +1,328 @@
1
+ """MCP server for Gildea market intelligence data.
2
+
3
+ Modes:
4
+ stdio (default): python -m gildea_sdk.mcp
5
+ sse: python -m gildea_sdk.mcp --sse
6
+ sse (custom): python -m gildea_sdk.mcp --sse --port 8001
7
+
8
+ Configuration via environment variables:
9
+ GILDEA_API_KEY - Required. API key for authentication.
10
+ GILDEA_API_BASE_URL - Base URL for the REST API (default: https://api.gildea.ai)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import asyncio
17
+ import json
18
+ import os
19
+
20
+ import httpx
21
+ from mcp.server import Server
22
+ from mcp.server.stdio import stdio_server
23
+ from mcp.types import TextContent, Tool
24
+
25
+ from gildea_sdk.mcp import tools
26
+
27
+ server = Server("gildea")
28
+ _http_client: httpx.AsyncClient | None = None
29
+
30
+
31
+ def _json_result(data: dict) -> list[TextContent]:
32
+ return [TextContent(type="text", text=json.dumps(data, default=str, indent=2))]
33
+
34
+
35
+ def _error_result(error: str) -> list[TextContent]:
36
+ return _json_result({"error": error})
37
+
38
+
39
+ def _ensure_client() -> httpx.AsyncClient:
40
+ global _http_client
41
+ if _http_client is None:
42
+ api_key = os.getenv("GILDEA_API_KEY", "")
43
+ base_url = os.getenv("GILDEA_API_BASE_URL", "https://api.gildea.ai")
44
+ if not api_key:
45
+ raise ValueError("GILDEA_API_KEY environment variable is required")
46
+ _http_client = httpx.AsyncClient(
47
+ base_url=base_url,
48
+ headers={"X-API-Key": api_key},
49
+ timeout=30.0,
50
+ )
51
+ return _http_client
52
+
53
+
54
+ @server.list_tools()
55
+ async def list_tools() -> list[Tool]:
56
+ return [
57
+ Tool(
58
+ name="search_text_units",
59
+ description="Semantic search across all of Gildea's verified extractions — thesis sentences, arguments, summaries, and claims. Text units are Gildea's own analytical extractions from source material, independently verified for factual accuracy. They are not direct quotes. Two modes: q='search query' finds text units matching a natural language question; similar_to='unit_id' finds text units semantically similar to a known unit (best for consensus mapping and cross-source synthesis). Use cases: 'What do multiple sources say about X?' → q with broad query. 'Who else is saying something like this claim?' → similar_to with unit ID. Returns sentence-level results with source attribution. Set recency_boost (0-1) to favor newer signals. Set verification_detail=full for complete verification metadata.",
60
+ inputSchema={
61
+ "type": "object",
62
+ "properties": {
63
+ "q": {"type": "string", "description": "Search query text. Required unless similar_to is provided."},
64
+ "similar_to": {"type": "string", "description": "Text unit ID to find similar units for. Uses the unit's stored embedding for pure vector similarity search. Required unless q is provided."},
65
+ "unit_type": {
66
+ "type": "string",
67
+ "enum": [
68
+ "thesis_sentence",
69
+ "argument_sentence",
70
+ "summary_sentence",
71
+ "analysis_claim",
72
+ "event_claim",
73
+ ],
74
+ "description": "Filter by text unit type",
75
+ },
76
+ "entity": {"type": "string", "description": "Filter by entity ID"},
77
+ "theme": {"type": "string", "description": "Filter by theme label"},
78
+ "published_after": {"type": "string", "description": "ISO 8601 date — only results published after this date"},
79
+ "published_before": {"type": "string", "description": "ISO 8601 date — only results published before this date"},
80
+ "recency_boost": {
81
+ "type": "number",
82
+ "description": "Recency weight (0=none, 1=max). Boosts newer signals in ranking.",
83
+ "default": 0.0,
84
+ "minimum": 0,
85
+ "maximum": 1,
86
+ },
87
+ "verification_detail": {
88
+ "type": "string",
89
+ "enum": ["full"],
90
+ "description": "Include full verification metadata",
91
+ },
92
+ "limit": {"type": "integer", "default": 10, "maximum": 25},
93
+ },
94
+ "oneOf": [
95
+ {"required": ["q"]},
96
+ {"required": ["similar_to"]},
97
+ ],
98
+ },
99
+ ),
100
+ Tool(
101
+ name="list_signals",
102
+ description="Find intelligence signals about the AI economy from 500+ expert sources. Each signal is a structured analysis or event report that has been decomposed into verified components (thesis → arguments → claims for analysis; summary → claims for events). This endpoint returns signal metadata — use get_signal_detail to access the full verified decomposition tree. All filters are optional. Without filters, returns the most recent signals sorted by date. Use cases: 'What's happening with [company]?' → filter by entity. 'What's new in AI regulation?' → filter by theme. 'Anything about open source models?' → text query. 'What's new today?' → no filter, most recent. Each result includes title, source, date, tags (value chain + market force), mentioned entities, and count of verified text units.",
103
+ inputSchema={
104
+ "type": "object",
105
+ "properties": {
106
+ "entity": {"type": "string", "description": "Entity ID filter"},
107
+ "theme": {"type": "string", "description": "Theme label filter"},
108
+ "q": {"type": "string", "description": "Text search query"},
109
+ "content_type": {"type": "string", "enum": ["analysis", "event"]},
110
+ "published_after": {"type": "string", "description": "ISO date"},
111
+ "published_before": {"type": "string", "description": "ISO date"},
112
+ "limit": {"type": "integer", "default": 25, "maximum": 50},
113
+ },
114
+ },
115
+ ),
116
+ Tool(
117
+ name="get_signal_detail",
118
+ description="Get the full verified reasoning chain for a signal. This is where Gildea's value lives. Analysis signals decompose into: thesis → supporting arguments → verified claims. Event signals decompose into: summary → verified claims. Each piece has been independently verified against source evidence. Use this tool liberally — it's the difference between showing a user metadata and showing them the actual verified intelligence. Set include=evidence to see the source snippets each claim was verified against. Set verification_detail=full for complete verification metadata.",
119
+ inputSchema={
120
+ "type": "object",
121
+ "properties": {
122
+ "signal_id": {"type": "string", "description": "Signal ID"},
123
+ "include": {
124
+ "type": "string",
125
+ "enum": ["evidence"],
126
+ "description": "Include evidence snippets",
127
+ },
128
+ "verification_detail": {
129
+ "type": "string",
130
+ "enum": ["full"],
131
+ "description": "Include full verification metadata",
132
+ },
133
+ },
134
+ "required": ["signal_id"],
135
+ },
136
+ ),
137
+ Tool(
138
+ name="get_entity_profile",
139
+ description="Get an intelligence profile for a company, person, or product. Returns: whether the entity is rising, stable, or declining in expert coverage; how much attention it's getting relative to others (scale); whether the trend is statistically significant; and a priority assessment. Use this to understand an entity's trajectory before diving into specific signals. Accepts display name (e.g. 'NVIDIA') or entity ID. Note: entities with fewer than 8 mentions in the 12-week window return limited trend data.",
140
+ inputSchema={
141
+ "type": "object",
142
+ "properties": {
143
+ "name_or_id": {
144
+ "type": "string",
145
+ "description": "Entity name (fuzzy matched) or entity UUID",
146
+ },
147
+ },
148
+ "required": ["name_or_id"],
149
+ },
150
+ ),
151
+ Tool(
152
+ name="list_entities",
153
+ description="Discover which companies, people, and products are getting expert attention in the AI economy. Filter by trend direction (Rising/Stable/Declining), priority level, coverage scale, and more. Use this for discovery — finding what's worth paying attention to. Common patterns: 'What's trending?' → direction=Rising, confidence=Significant. 'What should I watch?' → priority=High. 'Any new entrants?' → direction=New.",
154
+ inputSchema={
155
+ "type": "object",
156
+ "properties": {
157
+ "q": {"type": "string", "description": "Fuzzy name search"},
158
+ "theme": {"type": "string", "description": "Filter by theme label"},
159
+ "type_primary": {
160
+ "type": "string",
161
+ "description": "Filter by entity type: 'Organization', 'Person', 'Product', etc.",
162
+ },
163
+ "sort": {
164
+ "type": "string",
165
+ "enum": ["signal_count", "first_seen", "trend"],
166
+ "default": "signal_count",
167
+ },
168
+ "direction": {
169
+ "type": "string",
170
+ "enum": ["Rising", "Stable", "Declining", "New"],
171
+ "description": "Filter by trend direction",
172
+ },
173
+ "confidence": {
174
+ "type": "string",
175
+ "enum": ["Significant", "Insignificant"],
176
+ "description": "Filter by trend statistical significance",
177
+ },
178
+ "stability": {
179
+ "type": "string",
180
+ "enum": ["Volatile", "Steady"],
181
+ "description": "Filter by coverage consistency",
182
+ },
183
+ "scale": {
184
+ "type": "string",
185
+ "enum": ["High", "Medium", "Low"],
186
+ "description": "Filter by share-of-voice scale",
187
+ },
188
+ "priority": {
189
+ "type": "string",
190
+ "enum": ["High", "Medium", "Low", "Negligible"],
191
+ "description": "Filter by combined priority assessment",
192
+ },
193
+ "limit": {"type": "integer", "default": 25, "maximum": 50},
194
+ },
195
+ },
196
+ ),
197
+ Tool(
198
+ name="get_themes",
199
+ description="List all taxonomy themes across two axes that categorize the AI economy. Value chain axis (where in the stack): Infrastructure, Foundation Models, Orchestration, Data & Labeling, Applications, Distribution. Market force axis (what's driving change): Capital & Investment, Regulatory & Legal, Competitive Dynamics, Talent & Labor, Geopolitical Strategy, Trust & Societal Impact. Each theme includes signal counts, trend direction, and priority. Use get_theme_detail for full trend analytics and co-occurring themes.",
200
+ inputSchema={
201
+ "type": "object",
202
+ "properties": {
203
+ "axis": {
204
+ "type": "string",
205
+ "enum": ["value_chain", "market_force"],
206
+ "description": "Filter by taxonomy axis",
207
+ },
208
+ },
209
+ },
210
+ ),
211
+ Tool(
212
+ name="get_theme_detail",
213
+ description="Get a specific theme's full trend analytics and co-occurring themes from both axes. Returns trend direction, statistical confidence, stability, priority with reasoning, and which themes from the other axis most frequently co-occur. Use this to understand how themes interconnect. Example: 'Foundation Models' co-occurring heavily with 'Competitive Dynamics' signals a competitive landscape shift in the model layer.",
214
+ inputSchema={
215
+ "type": "object",
216
+ "properties": {
217
+ "axis": {"type": "string", "enum": ["value_chain", "market_force"]},
218
+ "label": {"type": "string", "description": "Theme label"},
219
+ },
220
+ "required": ["axis", "label"],
221
+ },
222
+ ),
223
+ ]
224
+
225
+
226
+ @server.call_tool()
227
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
228
+ try:
229
+ client = _ensure_client()
230
+ except ValueError as e:
231
+ return _error_result(f"Authentication failed: {e}")
232
+
233
+ handlers = {
234
+ "search_text_units": lambda: tools.search_text_units(client, **arguments),
235
+ "list_signals": lambda: tools.list_signals(client, **arguments),
236
+ "get_signal_detail": lambda: tools.get_signal_detail(client, **arguments),
237
+ "get_entity_profile": lambda: tools.get_entity_profile(client, **arguments),
238
+ "list_entities": lambda: tools.list_entities(client, **arguments),
239
+ "get_themes": lambda: tools.get_themes(client, **arguments),
240
+ "get_theme_detail": lambda: tools.get_theme_detail(client, **arguments),
241
+ }
242
+
243
+ handler = handlers.get(name)
244
+ if not handler:
245
+ return _error_result(f"Unknown tool: {name}")
246
+
247
+ try:
248
+ result = await handler()
249
+ return _json_result(result)
250
+ except httpx.HTTPStatusError as e:
251
+ status = e.response.status_code
252
+ try:
253
+ body = e.response.json()
254
+ message = body.get("error", {}).get("message", e.response.text)
255
+ except Exception:
256
+ message = e.response.text
257
+ if status in (401, 403):
258
+ return _error_result(f"Authentication failed: {message}")
259
+ if status == 429:
260
+ return _error_result(f"Rate limit exceeded: {message}")
261
+ if status == 400:
262
+ return _error_result(message)
263
+ return _error_result(f"API error: {message}")
264
+ except httpx.RequestError as e:
265
+ return _error_result(f"API connection error: {e}")
266
+
267
+
268
+ # --- Transport modes ---
269
+
270
+
271
+ async def _run_stdio() -> None:
272
+ async with stdio_server() as (read_stream, write_stream):
273
+ await server.run(
274
+ read_stream, write_stream, server.create_initialization_options()
275
+ )
276
+
277
+
278
+ def _create_sse_app(host: str, port: int):
279
+ """Create a Starlette app that serves the MCP server over SSE."""
280
+ from mcp.server.sse import SseServerTransport
281
+ from starlette.applications import Starlette
282
+ from starlette.routing import Mount, Route
283
+
284
+ sse_transport = SseServerTransport("/messages/")
285
+
286
+ async def handle_sse(scope, receive, send):
287
+ async with sse_transport.connect_sse(scope, receive, send) as (
288
+ read_stream,
289
+ write_stream,
290
+ ):
291
+ await server.run(
292
+ read_stream, write_stream, server.create_initialization_options()
293
+ )
294
+
295
+ return Starlette(
296
+ routes=[
297
+ Route("/sse", app=handle_sse),
298
+ Mount("/messages/", app=sse_transport.handle_post_message),
299
+ ],
300
+ )
301
+
302
+
303
+ def _run_sse(host: str, port: int) -> None:
304
+ import uvicorn
305
+
306
+ app = _create_sse_app(host, port)
307
+ uvicorn.run(app, host=host, port=port)
308
+
309
+
310
+ def main() -> None:
311
+ parser = argparse.ArgumentParser(description="Gildea MCP Server")
312
+ parser.add_argument(
313
+ "--sse", action="store_true", help="Run in SSE mode (HTTP server)"
314
+ )
315
+ parser.add_argument("--host", default="0.0.0.0", help="SSE host (default: 0.0.0.0)")
316
+ parser.add_argument(
317
+ "--port", type=int, default=8001, help="SSE port (default: 8001)"
318
+ )
319
+ args = parser.parse_args()
320
+
321
+ if args.sse:
322
+ _run_sse(args.host, args.port)
323
+ else:
324
+ asyncio.run(_run_stdio())
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()
@@ -0,0 +1,164 @@
1
+ """MCP tool implementations — thin HTTP proxies to the REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import quote
7
+
8
+ import httpx
9
+
10
+
11
+ async def search_text_units(
12
+ client: httpx.AsyncClient,
13
+ *,
14
+ q: str | None = None,
15
+ similar_to: str | None = None,
16
+ unit_type: str | None = None,
17
+ entity: str | None = None,
18
+ theme: str | None = None,
19
+ published_after: str | None = None,
20
+ published_before: str | None = None,
21
+ recency_boost: float | None = None,
22
+ verification_detail: str | None = None,
23
+ limit: int = 10,
24
+ ) -> dict[str, Any]:
25
+ params: dict[str, Any] = {"limit": limit}
26
+ if q:
27
+ params["q"] = q
28
+ if similar_to:
29
+ params["similar_to"] = similar_to
30
+ if unit_type:
31
+ params["unit_type"] = unit_type
32
+ if entity:
33
+ params["entity"] = entity
34
+ if theme:
35
+ params["theme"] = theme
36
+ if published_after:
37
+ params["published_after"] = published_after
38
+ if published_before:
39
+ params["published_before"] = published_before
40
+ if recency_boost is not None:
41
+ params["recency_boost"] = recency_boost
42
+ if verification_detail:
43
+ params["verification_detail"] = verification_detail
44
+ resp = await client.get("/v1/search", params=params)
45
+ resp.raise_for_status()
46
+ return resp.json()
47
+
48
+
49
+ async def list_signals(
50
+ client: httpx.AsyncClient,
51
+ *,
52
+ entity: str | None = None,
53
+ theme: str | None = None,
54
+ q: str | None = None,
55
+ content_type: str | None = None,
56
+ published_after: str | None = None,
57
+ published_before: str | None = None,
58
+ limit: int = 25,
59
+ ) -> dict[str, Any]:
60
+ params: dict[str, Any] = {"limit": limit}
61
+ if entity:
62
+ params["entity"] = entity
63
+ if theme:
64
+ params["theme"] = theme
65
+ if q:
66
+ params["q"] = q
67
+ if content_type:
68
+ params["content_type"] = content_type
69
+ if published_after:
70
+ params["published_after"] = published_after
71
+ if published_before:
72
+ params["published_before"] = published_before
73
+ resp = await client.get("/v1/signals", params=params)
74
+ resp.raise_for_status()
75
+ return resp.json()
76
+
77
+
78
+ async def get_signal_detail(
79
+ client: httpx.AsyncClient,
80
+ *,
81
+ signal_id: str,
82
+ include: str | None = None,
83
+ verification_detail: str | None = None,
84
+ ) -> dict[str, Any]:
85
+ params: dict[str, str] = {}
86
+ if include:
87
+ params["include"] = include
88
+ if verification_detail:
89
+ params["verification_detail"] = verification_detail
90
+ resp = await client.get(f"/v1/signals/{quote(signal_id, safe='')}", params=params)
91
+ resp.raise_for_status()
92
+ return resp.json()
93
+
94
+
95
+ async def get_entity_profile(
96
+ client: httpx.AsyncClient,
97
+ *,
98
+ name_or_id: str,
99
+ ) -> dict[str, Any]:
100
+ resp = await client.get(f"/v1/entities/{quote(name_or_id, safe='')}")
101
+ resp.raise_for_status()
102
+ return resp.json()
103
+
104
+
105
+ async def list_entities(
106
+ client: httpx.AsyncClient,
107
+ *,
108
+ q: str | None = None,
109
+ theme: str | None = None,
110
+ type_primary: str | None = None,
111
+ sort: str = "signal_count",
112
+ limit: int = 25,
113
+ direction: str | None = None,
114
+ confidence: str | None = None,
115
+ stability: str | None = None,
116
+ scale: str | None = None,
117
+ priority: str | None = None,
118
+ ) -> dict[str, Any]:
119
+ params: dict[str, Any] = {"sort": sort, "limit": limit}
120
+ if q:
121
+ params["q"] = q
122
+ if theme:
123
+ params["theme"] = theme
124
+ if type_primary:
125
+ params["type_primary"] = type_primary
126
+ if direction:
127
+ params["direction"] = direction
128
+ if confidence:
129
+ params["confidence"] = confidence
130
+ if stability:
131
+ params["stability"] = stability
132
+ if scale:
133
+ params["scale"] = scale
134
+ if priority:
135
+ params["priority"] = priority
136
+ resp = await client.get("/v1/entities", params=params)
137
+ resp.raise_for_status()
138
+ return resp.json()
139
+
140
+
141
+ async def get_themes(
142
+ client: httpx.AsyncClient,
143
+ *,
144
+ axis: str | None = None,
145
+ ) -> dict[str, Any]:
146
+ params: dict[str, str] = {}
147
+ if axis:
148
+ params["axis"] = axis
149
+ resp = await client.get("/v1/themes", params=params)
150
+ resp.raise_for_status()
151
+ return resp.json()
152
+
153
+
154
+ async def get_theme_detail(
155
+ client: httpx.AsyncClient,
156
+ *,
157
+ axis: str,
158
+ label: str,
159
+ ) -> dict[str, Any]:
160
+ resp = await client.get(
161
+ f"/v1/themes/{quote(axis, safe='')}/{quote(label, safe='')}"
162
+ )
163
+ resp.raise_for_status()
164
+ return resp.json()
@@ -0,0 +1,77 @@
1
+ """Shared request helpers and auto-pagination for the Gildea SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import quote
6
+
7
+ from .exceptions import (
8
+ APIError,
9
+ AuthenticationError,
10
+ BadRequestError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ )
14
+
15
+ __all__ = ["_request", "_paginate", "_clean", "_quote"]
16
+
17
+
18
+ def _clean(params: dict) -> dict:
19
+ """Remove None values from a dict of query parameters."""
20
+ return {k: v for k, v in params.items() if v is not None}
21
+
22
+
23
+ def _quote(segment: str) -> str:
24
+ """URL-encode a path segment."""
25
+ return quote(segment, safe="")
26
+
27
+
28
+ def _request(client, method: str, path: str, *, params: dict | None = None):
29
+ """Send a request and return parsed JSON, raising typed errors on failure."""
30
+ resp = client.request(method, path, params=params)
31
+
32
+ if resp.status_code == 204:
33
+ return None
34
+
35
+ try:
36
+ body = resp.json()
37
+ except Exception:
38
+ if resp.is_success:
39
+ raise APIError(
40
+ f"Invalid JSON in response: {resp.text[:200]}",
41
+ code="invalid_response",
42
+ status=resp.status_code,
43
+ )
44
+ raise APIError(resp.text[:200] or "Unknown error", code="server_error", status=resp.status_code)
45
+
46
+ if resp.is_success:
47
+ return body
48
+
49
+ error = body.get("error", {})
50
+ msg = error.get("message", resp.text)
51
+ code = error.get("code")
52
+ status = resp.status_code
53
+
54
+ if status in (401, 403):
55
+ raise AuthenticationError(msg, code=code, status=status)
56
+ if status == 404:
57
+ raise NotFoundError(msg, code=code, status=status)
58
+ if status == 429:
59
+ retry_after = resp.headers.get("Retry-After")
60
+ if retry_after is not None:
61
+ retry_after = int(retry_after)
62
+ raise RateLimitError(msg, code=code, status=status, retry_after=retry_after)
63
+ if status == 400:
64
+ raise BadRequestError(msg, code=code, status=status)
65
+
66
+ raise APIError(msg, code=code, status=status)
67
+
68
+
69
+ def _paginate(client, path: str, params: dict):
70
+ """Auto-paginating generator that yields items from paginated list endpoints."""
71
+ params = dict(params) # copy so we can mutate
72
+ while True:
73
+ resp = _request(client, "GET", path, params=_clean(params))
74
+ yield from resp["data"]
75
+ if not resp.get("has_more"):
76
+ break
77
+ params["cursor"] = resp["cursor"]
@@ -0,0 +1,6 @@
1
+ from .entities import EntitiesResource
2
+ from .search import SearchResource
3
+ from .signals import SignalsResource
4
+ from .themes import ThemesResource
5
+
6
+ __all__ = ["SignalsResource", "EntitiesResource", "ThemesResource", "SearchResource"]
@@ -0,0 +1,50 @@
1
+ """Entities resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..pagination import _clean, _paginate, _quote, _request
6
+
7
+
8
+ class EntitiesResource:
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def list(
13
+ self,
14
+ *,
15
+ theme=None,
16
+ type_primary=None,
17
+ q=None,
18
+ sort="signal_count",
19
+ cursor=None,
20
+ limit=25,
21
+ direction=None,
22
+ confidence=None,
23
+ stability=None,
24
+ scale=None,
25
+ priority=None,
26
+ ):
27
+ params = _clean(
28
+ {
29
+ "theme": theme,
30
+ "type_primary": type_primary,
31
+ "q": q,
32
+ "sort": sort,
33
+ "cursor": cursor,
34
+ "limit": limit,
35
+ "direction": direction,
36
+ "confidence": confidence,
37
+ "stability": stability,
38
+ "scale": scale,
39
+ "priority": priority,
40
+ }
41
+ )
42
+ return _request(self._client, "GET", "/v1/entities", params=params)
43
+
44
+ def get(self, name_or_id):
45
+ path = f"/v1/entities/{_quote(name_or_id)}"
46
+ return _request(self._client, "GET", path)
47
+
48
+ def list_all(self, **kwargs):
49
+ """Auto-paginating iterator that yields every entity matching the filters."""
50
+ return _paginate(self._client, "/v1/entities", kwargs)
@@ -0,0 +1,44 @@
1
+ """Search resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..pagination import _clean, _request
6
+
7
+
8
+ class SearchResource:
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def query(
13
+ self,
14
+ q=None,
15
+ *,
16
+ similar_to=None,
17
+ unit_type=None,
18
+ entity=None,
19
+ theme=None,
20
+ published_after=None,
21
+ published_before=None,
22
+ recency_boost=None,
23
+ verification_detail=None,
24
+ limit=10,
25
+ ):
26
+ if not q and not similar_to:
27
+ raise ValueError("Exactly one of q or similar_to is required")
28
+ if q and similar_to:
29
+ raise ValueError("Exactly one of q or similar_to is required")
30
+ params = _clean(
31
+ {
32
+ "q": q,
33
+ "similar_to": similar_to,
34
+ "unit_type": unit_type,
35
+ "entity": entity,
36
+ "theme": theme,
37
+ "published_after": published_after,
38
+ "published_before": published_before,
39
+ "recency_boost": recency_boost,
40
+ "verification_detail": verification_detail,
41
+ "limit": limit,
42
+ }
43
+ )
44
+ return _request(self._client, "GET", "/v1/search", params=params)
@@ -0,0 +1,47 @@
1
+ """Signals resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..pagination import _clean, _paginate, _quote, _request
6
+
7
+
8
+ class SignalsResource:
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def list(
13
+ self,
14
+ *,
15
+ entity=None,
16
+ theme=None,
17
+ q=None,
18
+ content_type=None,
19
+ published_after=None,
20
+ published_before=None,
21
+ cursor=None,
22
+ limit=25,
23
+ ):
24
+ params = _clean(
25
+ {
26
+ "entity": entity,
27
+ "theme": theme,
28
+ "q": q,
29
+ "content_type": content_type,
30
+ "published_after": published_after,
31
+ "published_before": published_before,
32
+ "cursor": cursor,
33
+ "limit": limit,
34
+ }
35
+ )
36
+ return _request(self._client, "GET", "/v1/signals", params=params)
37
+
38
+ def get(self, signal_id, *, include=None, verification_detail=None):
39
+ path = f"/v1/signals/{_quote(signal_id)}"
40
+ params = _clean(
41
+ {"include": include, "verification_detail": verification_detail}
42
+ )
43
+ return _request(self._client, "GET", path, params=params)
44
+
45
+ def list_all(self, **kwargs):
46
+ """Auto-paginating iterator that yields every signal matching the filters."""
47
+ return _paginate(self._client, "/v1/signals", kwargs)
@@ -0,0 +1,18 @@
1
+ """Themes resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..pagination import _clean, _quote, _request
6
+
7
+
8
+ class ThemesResource:
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def list(self, *, axis=None):
13
+ params = _clean({"axis": axis})
14
+ return _request(self._client, "GET", "/v1/themes", params=params)
15
+
16
+ def get(self, axis, label):
17
+ path = f"/v1/themes/{_quote(axis)}/{_quote(label)}"
18
+ return _request(self._client, "GET", path)