clevertech-mcp-server 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.
File without changes
clevertech_mcp/auth.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ Authentication helpers for the CleverTech MCP server.
3
+
4
+ Handles extraction of client IP, user API keys from request headers,
5
+ and resolution of the upstream API key.
6
+ """
7
+
8
+ from typing import Optional, TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from starlette.requests import Request
12
+
13
+
14
+ # MCP tools receive a *context* object (starlette Request or similar).
15
+ # We use duck-typing to support both Starlette's Request and MCP's
16
+ # RequestContext / MiddlewareContext.
17
+
18
+
19
+ def _extract_client_ip(context) -> str:
20
+ """Extract the client IP address from an MCP/starlette context.
21
+
22
+ Prefers X-Forwarded-For (first entry) when present; falls back to
23
+ ``context.request.client.host`` or ``"unknown"``.
24
+ """
25
+ # Starlette-style request
26
+ request: Optional["Request"] = getattr(context, "request", None)
27
+ if request is not None:
28
+ forwarded = request.headers.get("X-Forwarded-For")
29
+ if forwarded:
30
+ # X-Forwarded-For can contain a comma-separated list
31
+ return forwarded.split(",")[0].strip()
32
+ if request.client is not None:
33
+ return request.client.host
34
+ return "unknown"
35
+
36
+
37
+ def _get_user_api_key(context) -> Optional[str]:
38
+ """Extract a user-supplied API key from the request context.
39
+
40
+ Looks for an ``Authorization: Bearer <token>`` header. Returns
41
+ ``None`` if no key is present.
42
+ """
43
+ import os
44
+
45
+ request = getattr(context, "request", None)
46
+ if request is not None:
47
+ auth_header: Optional[str] = request.headers.get("Authorization", "")
48
+ if auth_header.startswith("Bearer "):
49
+ return auth_header[7:] # strip "Bearer "
50
+ return os.getenv("CLEVERTECH_API_KEY")
51
+
52
+
53
+ def get_upstream_key(
54
+ user_api_key: Optional[str],
55
+ server_api_key: Optional[str],
56
+ ) -> Optional[str]:
57
+ """Return the API key to use for upstream CleverTech API calls.
58
+
59
+ Prefers the user-provided key (authenticated user) over the server-
60
+ wide key (anonymous / default). Returns ``None`` if neither is set.
61
+ """
62
+ if user_api_key:
63
+ return user_api_key
64
+ return server_api_key
65
+
66
+
67
+ def is_authenticated(user_api_key: Optional[str]) -> bool:
68
+ """Check if a user API key was provided."""
69
+ return bool(user_api_key) # Empty string is not authenticated
@@ -0,0 +1,54 @@
1
+ """
2
+ CleverTech API client — async httpx wrapper.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+ import httpx
7
+
8
+
9
+ class CleverTechClient:
10
+ """Async HTTP client for the CleverTech API."""
11
+
12
+ def __init__(self, base_url: str, api_key: Optional[str] = None) -> None:
13
+ self._base_url = base_url.rstrip("/")
14
+ self._api_key = api_key
15
+ self._client: Optional[httpx.AsyncClient] = None
16
+
17
+ def _get_client(self) -> httpx.AsyncClient:
18
+ """Lazily initialise and return the underlying httpx.AsyncClient."""
19
+ if self._client is None:
20
+ headers: dict[str, str] = {
21
+ "Accept": "application/json",
22
+ }
23
+ if self._api_key:
24
+ headers["Authorization"] = f"Bearer {self._api_key}"
25
+ self._client = httpx.AsyncClient(
26
+ base_url=self._base_url,
27
+ headers=headers,
28
+ timeout=30.0,
29
+ )
30
+ return self._client
31
+
32
+ async def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
33
+ """Perform an async GET request and return the parsed JSON body."""
34
+ client = self._get_client()
35
+ response = await client.get(path, params=params)
36
+ response.raise_for_status()
37
+ return response.json()
38
+
39
+ async def post(
40
+ self,
41
+ path: str,
42
+ json: Optional[dict[str, Any]] = None,
43
+ ) -> Any:
44
+ """Perform an async POST request and return the parsed JSON body."""
45
+ client = self._get_client()
46
+ response = await client.post(path, json=json)
47
+ response.raise_for_status()
48
+ return response.json()
49
+
50
+ async def close(self) -> None:
51
+ """Close the underlying HTTP client, if initialised."""
52
+ if self._client is not None:
53
+ await self._client.aclose()
54
+ self._client = None
@@ -0,0 +1,35 @@
1
+ """
2
+ CleverTech MCP Server — configuration loader.
3
+
4
+ Loads settings from environment variables via python-dotenv.
5
+ """
6
+
7
+ import os
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+
13
+ def load_config() -> dict:
14
+ """Load and return the server configuration as a dict.
15
+
16
+ Environment variables read:
17
+ CLEVERTECH_API_URL — base URL for the CleverTech API
18
+ CLEVERTECH_API_KEY — API key for authenticated requests
19
+ PORT — port for SSE transport
20
+ TRANSPORT — transport mode (stdio or sse)
21
+ LOG_LEVEL — logging level
22
+ RATE_LIMIT_ANON_DAILY — max anonymous requests per day
23
+ RATE_LIMIT_ANON_BURST — max anonymous requests per minute
24
+ SENTRY_DSN — Sentry DSN for error tracking
25
+ """
26
+ return {
27
+ "api_url": os.getenv("CLEVERTECH_API_URL", "https://clevertech.ca"),
28
+ "api_key": os.getenv("CLEVERTECH_API_KEY"),
29
+ "port": int(os.getenv("PORT", "8001")),
30
+ "transport": os.getenv("TRANSPORT", "stdio"),
31
+ "log_level": os.getenv("LOG_LEVEL", "INFO"),
32
+ "rate_limit_anon_daily": int(os.getenv("RATE_LIMIT_ANON_DAILY", "50")),
33
+ "rate_limit_anon_burst": int(os.getenv("RATE_LIMIT_ANON_BURST", "10")),
34
+ "sentry_dsn": os.getenv("SENTRY_DSN"),
35
+ }
@@ -0,0 +1,100 @@
1
+ """
2
+ Local in-memory rate limiter for anonymous users.
3
+
4
+ Tracks requests per source IP using a sliding-window approach:
5
+ - Daily limit (24-hour sliding window)
6
+ - Burst limit (1-minute sliding window)
7
+ """
8
+
9
+ import time
10
+ import threading
11
+ from typing import Optional
12
+
13
+
14
+ class LocalRateLimiter:
15
+ """Thread-safe in-memory rate limiter keyed by source IP."""
16
+
17
+ def __init__(
18
+ self,
19
+ daily_limit: int = 50,
20
+ burst_per_minute: int = 10,
21
+ ) -> None:
22
+ self._daily_limit = daily_limit
23
+ self._burst_per_minute = burst_per_minute
24
+ self._storage: dict[str, list[tuple[float, int]]] = {}
25
+ self._lock = threading.Lock()
26
+
27
+ # ------------------------------------------------------------------
28
+ # Internal helpers
29
+ # ------------------------------------------------------------------
30
+
31
+ def _prune(self, key: str, now: float) -> None:
32
+ """Remove entries older than 24 hours from *key*'s history."""
33
+ if key not in self._storage:
34
+ return
35
+ cutoff = now - 86400.0 # 24 hours
36
+ self._storage[key] = [
37
+ (ts, cost) for ts, cost in self._storage[key] if ts > cutoff
38
+ ]
39
+ if not self._storage[key]:
40
+ del self._storage[key]
41
+
42
+ def _daily_usage(self, key: str) -> int:
43
+ """Total cost accumulated by *key* in the last 24 hours."""
44
+ return sum(cost for _, cost in self._storage.get(key, []))
45
+
46
+ def _burst_usage(self, key: str, now: float) -> int:
47
+ """Total cost accumulated by *key* in the last 60 seconds."""
48
+ cutoff = now - 60.0
49
+ return sum(
50
+ cost
51
+ for ts, cost in self._storage.get(key, [])
52
+ if ts > cutoff
53
+ )
54
+
55
+ # ------------------------------------------------------------------
56
+ # Public API
57
+ # ------------------------------------------------------------------
58
+
59
+ def check(self, source_ip: str, cost: int = 1) -> tuple[bool, str]:
60
+ """Check whether *source_ip* is allowed to proceed.
61
+
62
+ Returns ``(True, "")`` if allowed, or ``(False, message)`` if
63
+ the request would exceed a rate limit.
64
+ """
65
+ now = time.time()
66
+ with self._lock:
67
+ self._prune(source_ip, now)
68
+
69
+ daily = self._daily_usage(source_ip)
70
+ if daily + cost > self._daily_limit:
71
+ remaining = self._daily_limit - daily
72
+ return False, (
73
+ f"Daily rate limit exceeded. "
74
+ f"{max(0, remaining)} of {self._daily_limit} requests remaining."
75
+ )
76
+
77
+ burst = self._burst_usage(source_ip, now)
78
+ if burst + cost > self._burst_per_minute:
79
+ return False, (
80
+ f"Burst rate limit exceeded. "
81
+ f"Max {self._burst_per_minute} requests per minute."
82
+ )
83
+
84
+ # Record the request
85
+ self._storage.setdefault(source_ip, []).append((now, cost))
86
+ return True, ""
87
+
88
+ def remaining(self, source_ip: str) -> dict[str, int]:
89
+ """Return a summary of remaining capacity for *source_ip*."""
90
+ now = time.time()
91
+ with self._lock:
92
+ self._prune(source_ip, now)
93
+ daily = self._daily_usage(source_ip)
94
+ burst = self._burst_usage(source_ip, now)
95
+ return {
96
+ "daily_used": daily,
97
+ "daily_remaining": max(0, self._daily_limit - daily),
98
+ "daily_limit": self._daily_limit,
99
+ "burst_remaining": max(0, self._burst_per_minute - burst),
100
+ }
@@ -0,0 +1,81 @@
1
+ """
2
+ CleverTech MCP Server — main entry point.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import argparse
8
+ import json
9
+ from mcp.server.fastmcp import FastMCP
10
+ from starlette.responses import JSONResponse
11
+ from starlette.routing import Route
12
+
13
+ from clevertech_mcp.config import load_config
14
+ from clevertech_mcp.client import CleverTechClient
15
+ from clevertech_mcp.tools import register_all_tools
16
+
17
+
18
+ async def health_endpoint(request):
19
+ """Health check endpoint for Railway deployment."""
20
+ return JSONResponse({"status": "ok", "service": "clevertech-mcp"})
21
+
22
+
23
+ def create_server(config: dict) -> FastMCP:
24
+ """Build and return a fully-configured FastMCP server instance."""
25
+ mcp = FastMCP("clevertech-mcp")
26
+
27
+ client = CleverTechClient(
28
+ base_url=config["api_url"],
29
+ api_key=config.get("api_key"),
30
+ )
31
+
32
+ register_all_tools(mcp, client, config)
33
+ return mcp
34
+
35
+
36
+ def main():
37
+ """CLI entry point for the CleverTech MCP server."""
38
+ parser = argparse.ArgumentParser(
39
+ description="CleverTech MCP Server — Canadian government data APIs"
40
+ )
41
+ parser.add_argument(
42
+ "--transport",
43
+ choices=["stdio", "sse"],
44
+ default=os.getenv("TRANSPORT", "stdio"),
45
+ help="Transport mode (default: %(default)s)",
46
+ )
47
+ parser.add_argument(
48
+ "--host",
49
+ default=os.getenv("HOST", "0.0.0.0"),
50
+ help="Host to bind when using SSE transport (default: %(default)s)",
51
+ )
52
+ parser.add_argument(
53
+ "--port",
54
+ type=int,
55
+ default=int(os.getenv("PORT", "8001")),
56
+ help="Port to bind when using SSE transport (default: %(default)s)",
57
+ )
58
+ args = parser.parse_args()
59
+
60
+ config = load_config()
61
+ config["host"] = args.host
62
+ config["port"] = args.port
63
+
64
+ mcp = create_server(config)
65
+
66
+ if args.transport == "sse":
67
+ # Register /health endpoint for Railway health checks
68
+ sse_app = mcp.sse_app()
69
+ if not any(
70
+ getattr(r, "path", "") == "/health"
71
+ for r in getattr(sse_app, "routes", [])
72
+ ):
73
+ sse_app.router.routes.insert(0, Route("/health", endpoint=health_endpoint))
74
+ import uvicorn
75
+ uvicorn.run(sse_app, host=args.host, port=args.port)
76
+ else:
77
+ mcp.run(transport="stdio")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,53 @@
1
+ """
2
+ Aggregate all tool registrations onto a single FastMCP instance.
3
+ """
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+ from clevertech_mcp.client import CleverTechClient
7
+
8
+
9
+ def register_all_tools(mcp: FastMCP, client: CleverTechClient, config: dict) -> None:
10
+ """Wire up every tool category to the FastMCP server."""
11
+ # Lazy imports so that individual tool modules can be worked on in
12
+ # parallel without one missing module breaking the whole scaffold.
13
+ try:
14
+ from clevertech_mcp.tools.property import register_property_tools
15
+ register_property_tools(mcp, client, config)
16
+ except ImportError:
17
+ pass
18
+
19
+ try:
20
+ from clevertech_mcp.tools.dls import register_dls_tools
21
+ register_dls_tools(mcp, client, config)
22
+ except ImportError:
23
+ pass
24
+
25
+ try:
26
+ from clevertech_mcp.tools.building import register_building_tools
27
+ register_building_tools(mcp, client, config)
28
+ except ImportError:
29
+ pass
30
+
31
+ try:
32
+ from clevertech_mcp.tools.zoning import register_zoning_tools
33
+ register_zoning_tools(mcp, client, config)
34
+ except ImportError:
35
+ pass
36
+
37
+ try:
38
+ from clevertech_mcp.tools.business import register_business_tools
39
+ register_business_tools(mcp, client, config)
40
+ except ImportError:
41
+ pass
42
+
43
+ try:
44
+ from clevertech_mcp.tools.geo import register_geo_tools
45
+ register_geo_tools(mcp, client, config)
46
+ except ImportError:
47
+ pass
48
+
49
+ try:
50
+ from clevertech_mcp.tools.meta import register_meta_tools
51
+ register_meta_tools(mcp, client, config)
52
+ except ImportError:
53
+ pass
@@ -0,0 +1,95 @@
1
+ """Building permit MCP tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+ from clevertech_mcp.client import CleverTechClient
5
+
6
+
7
+ def register_building_tools(mcp: FastMCP, client: CleverTechClient, config: dict):
8
+ """Register building permit tools."""
9
+
10
+ @mcp.tool(
11
+ name="building_permit_search",
12
+ description="Search building permits by address, contractor, applicant, or permit number across 13+ Canadian cities. Returns permit details including type, value, status, and dates.",
13
+ )
14
+ async def building_permit_search(
15
+ city: str,
16
+ q: str,
17
+ permit_type: str = None,
18
+ limit: int = 20,
19
+ ) -> str:
20
+ """Search building permits.
21
+
22
+ Args:
23
+ city: City slug
24
+ q: Search query (address, contractor, applicant, or permit number)
25
+ permit_type: Filter by permit type (e.g., 'Building', 'Demolition', 'Electrical')
26
+ limit: Max results (1-200, default 20)
27
+ """
28
+ params = {"q": q, "limit": min(limit, 200)}
29
+ if permit_type:
30
+ params["permit_type"] = permit_type
31
+
32
+ data = await client.get(f"/api/{city}/building/search", params=params)
33
+
34
+ results = data.get("results", [])
35
+ total = data.get("total", 0)
36
+ message = data.get("_message", "")
37
+
38
+ lines = [f"Found {len(results)} of {total} building permits"]
39
+ if message:
40
+ lines.append(f"\n{message}")
41
+
42
+ for r in results:
43
+ lines.extend([
44
+ "",
45
+ "---",
46
+ f"Permit: {r.get('permit_number', 'N/A')}",
47
+ f"Type: {r.get('permit_type', 'N/A')}",
48
+ f"Status: {r.get('status', 'N/A')}",
49
+ f"Address: {r.get('address', 'N/A')}",
50
+ f"Applicant: {r.get('applicant', 'N/A')}",
51
+ f"Value: ${r.get('job_value', 0):,.0f}" if r.get('job_value') is not None else "Value: N/A",
52
+ f"Issued: {r.get('issued_date', 'N/A')}",
53
+ ])
54
+
55
+ return "\n".join(lines)
56
+
57
+ @mcp.tool(
58
+ name="building_permit_recent",
59
+ description="Get the most recently issued building permits for a city. Useful for monitoring new construction activity.",
60
+ )
61
+ async def building_permit_recent(
62
+ city: str,
63
+ limit: int = 20,
64
+ ) -> str:
65
+ """Get recently issued permits.
66
+
67
+ Args:
68
+ city: City slug
69
+ limit: Max results (1-100, default 20)
70
+ """
71
+ data = await client.get(
72
+ f"/api/{city}/building/recent",
73
+ params={"limit": min(limit, 100)},
74
+ )
75
+
76
+ results = data.get("results", [])
77
+ message = data.get("_message", "")
78
+
79
+ lines = [f"Recent building permits ({len(results)})"]
80
+ if message:
81
+ lines.append(f"\n{message}")
82
+
83
+ for r in results:
84
+ lines.extend([
85
+ "",
86
+ "---",
87
+ f"Permit: {r.get('permit_number', 'N/A')}",
88
+ f"Type: {r.get('permit_type', 'N/A')}",
89
+ f"Status: {r.get('status', 'N/A')}",
90
+ f"Address: {r.get('address', 'N/A')}",
91
+ f"Value: ${r.get('job_value', 0):,.0f}" if r.get('job_value') is not None else "Value: N/A",
92
+ f"Issued: {r.get('issued_date', 'N/A')}",
93
+ ])
94
+
95
+ return "\n".join(lines)
@@ -0,0 +1,60 @@
1
+ """Business registry MCP tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+ from clevertech_mcp.client import CleverTechClient
5
+
6
+
7
+ def register_business_tools(mcp: FastMCP, client: CleverTechClient, config: dict):
8
+ """Register business registry tools."""
9
+
10
+ @mcp.tool(
11
+ name="business_registry_search",
12
+ description="Search the Canadian federal business registry. Find corporations by name, province, city, status, or industry act.",
13
+ )
14
+ async def business_registry_search(
15
+ q: str,
16
+ province: str = None,
17
+ city: str = None,
18
+ status: str = None,
19
+ limit: int = 25,
20
+ ) -> str:
21
+ """Search the business registry.
22
+
23
+ Args:
24
+ q: Corporation name or keyword
25
+ province: Two-letter province code (AB, BC, ON, QC, etc.)
26
+ city: City name
27
+ status: Corporation status (Active, Dissolved, etc.)
28
+ limit: Max results (1-100, default 25)
29
+ """
30
+ params = {"q": q, "limit": min(limit, 100)}
31
+ if province:
32
+ params["province"] = province
33
+ if city:
34
+ params["city"] = city
35
+ if status:
36
+ params["status"] = status
37
+
38
+ data = await client.get("/api/registry/search", params=params)
39
+
40
+ results = data.get("results", [])
41
+ total = data.get("total", len(results))
42
+ message = data.get("_message", "")
43
+
44
+ lines = [f"Found {len(results)} of {total} corporations"]
45
+ if message:
46
+ lines.append(f"\n{message}")
47
+
48
+ for r in results:
49
+ lines.extend([
50
+ "",
51
+ "---",
52
+ f"Name: {r.get('corporation_name', r.get('name', 'N/A'))}",
53
+ f"Number: {r.get('corporation_number', r.get('number', 'N/A'))}",
54
+ f"Status: {r.get('status', 'N/A')}",
55
+ f"Jurisdiction: {r.get('jurisdiction', r.get('province', 'N/A'))}",
56
+ ])
57
+ if r.get('registered_address'):
58
+ lines.append(f"Address: {r['registered_address']}")
59
+
60
+ return "\n".join(lines)
@@ -0,0 +1,119 @@
1
+ """DLS conversion MCP tools.
2
+
3
+ Provides GPS ↔ Dominion Land Survey (DLS) coordinate conversion for Western
4
+ Canadian provinces (Alberta, Saskatchewan, Manitoba).
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+ from clevertech_mcp.client import CleverTechClient
11
+
12
+
13
+ def register_dls_tools(mcp: FastMCP, client: CleverTechClient, config: dict) -> None:
14
+ """Register DLS conversion tools on the FastMCP server."""
15
+
16
+ @mcp.tool(
17
+ name="dls_convert",
18
+ description=(
19
+ "Convert between GPS coordinates and Dominion Land Survey (DLS) "
20
+ "grid system used in Western Canada. Supports GPS→DLS and "
21
+ "DLS→GPS directions."
22
+ ),
23
+ )
24
+ async def dls_convert(
25
+ direction: str,
26
+ lat: Optional[float] = None,
27
+ lon: Optional[float] = None,
28
+ dls_string: Optional[str] = None,
29
+ province: Optional[str] = None,
30
+ ) -> str:
31
+ """Convert a single coordinate between GPS and DLS.
32
+
33
+ Args:
34
+ direction: 'gps_to_dls' or 'dls_to_gps'.
35
+ lat: Latitude (required for gps_to_dls).
36
+ lon: Longitude (required for gps_to_dls).
37
+ dls_string: DLS grid reference (required for dls_to_gps).
38
+ province: Province code AB/SK/MB (optional, auto-detected).
39
+ """
40
+ if direction == "gps_to_dls":
41
+ if lat is None or lon is None:
42
+ return "Error: lat and lon are required for gps_to_dls direction."
43
+ payload: dict = {"lat": lat, "lon": lon}
44
+ if province:
45
+ payload["province"] = province
46
+ data = await client.post("/api/v1/convert/gps-to-dls", json=payload)
47
+ else:
48
+ if not dls_string:
49
+ return "Error: dls_string is required for dls_to_gps direction."
50
+ payload = {"dls_string": dls_string}
51
+ if province:
52
+ payload["province"] = province
53
+ data = await client.post("/api/v1/convert/dls-to-gps", json=payload)
54
+
55
+ lines: list[str] = []
56
+ if data.get("dls"):
57
+ lines.append(f"DLS: {data['dls']}")
58
+ if data.get("province"):
59
+ lines.append(f"Province: {data['province']}")
60
+ if "lat" in data and "lon" in data:
61
+ lines.append(f"GPS: {data['lat']}, {data['lon']}")
62
+ if data.get("confidence"):
63
+ lines.append(f"Confidence: {data['confidence']}")
64
+ if data.get("distance_km"):
65
+ lines.append(f"Distance to grid center: {data['distance_km']} km")
66
+ if data.get("_message"):
67
+ lines.append(f"\n{data['message']}")
68
+
69
+ return "\n".join(lines) if lines else "No result returned."
70
+
71
+ @mcp.tool(
72
+ name="dls_batch",
73
+ description=(
74
+ "Convert multiple GPS coordinates to DLS or multiple DLS strings "
75
+ "to GPS in a single batch request. Up to 100 items per batch."
76
+ ),
77
+ )
78
+ async def dls_batch(
79
+ direction: str,
80
+ items: list[dict],
81
+ province: Optional[str] = None,
82
+ ) -> str:
83
+ """Batch convert coordinates.
84
+
85
+ Args:
86
+ direction: 'gps_to_dls' or 'dls_to_gps'.
87
+ items: List of coordinate objects.
88
+ gps_to_dls: [{"lat": 51.0, "lon": -114.0}, ...]
89
+ dls_to_gps: [{"dls_string": "NW-16-24-1-W5"}, ...]
90
+ province: Province code (optional).
91
+ """
92
+ if not items:
93
+ return "Error: items list is empty."
94
+ if len(items) > 100:
95
+ return "Error: batch limited to 100 items."
96
+
97
+ payload: dict = {"direction": direction, "items": items}
98
+ if province:
99
+ payload["province"] = province
100
+
101
+ data = await client.post("/api/v1/convert/batch", json=payload)
102
+
103
+ results = data.get("results", [])
104
+ lines: list[str] = [f"Batch result: {data.get('count', 0)} conversions"]
105
+
106
+ for i, r in enumerate(results[:20]):
107
+ if r.get("dls"):
108
+ line = f"{i + 1}. {r['dls']}"
109
+ if "lat" in r and "lon" in r:
110
+ line += f" → ({r['lat']}, {r['lon']})"
111
+ lines.append(line)
112
+
113
+ if len(results) > 20:
114
+ lines.append(f"\n... and {len(results) - 20} more")
115
+
116
+ if data.get("_message"):
117
+ lines.append(f"\n{data['message']}")
118
+
119
+ return "\n".join(lines)
@@ -0,0 +1,43 @@
1
+ """Geocoding MCP tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+ from clevertech_mcp.client import CleverTechClient
5
+
6
+
7
+ def register_geo_tools(mcp: FastMCP, client: CleverTechClient, config: dict):
8
+ """Register geocoding tools."""
9
+
10
+ @mcp.tool(
11
+ name="reverse_geocode",
12
+ description="Convert GPS coordinates to a human-readable address, city, neighborhood, province, and DLS grid reference. Works across all Canadian cities in the CleverTech database.",
13
+ )
14
+ async def reverse_geocode(lat: float, lon: float) -> str:
15
+ """Reverse geocode GPS coordinates.
16
+
17
+ Args:
18
+ lat: Latitude
19
+ lon: Longitude
20
+ """
21
+ data = await client.get(
22
+ "/api/v1/geocode/reverse",
23
+ params={"lat": lat, "lon": lon},
24
+ )
25
+
26
+ lines = [
27
+ f"Address: {data.get('address', 'N/A')}",
28
+ f"City: {data.get('city', 'N/A')}",
29
+ f"Province: {data.get('province', 'N/A')}",
30
+ f"Postal Code: {data.get('postal_code', data.get('postcode', 'N/A'))}",
31
+ f"Neighborhood: {data.get('neighborhood', data.get('neighbourhood', 'N/A'))}",
32
+ ]
33
+
34
+ if data.get("dls"):
35
+ lines.append(f"DLS: {data['dls']}")
36
+ if data.get("lat") and data.get("lon"):
37
+ lines.append(f"GPS: {data['lat']}, {data['lon']}")
38
+ if data.get("confidence"):
39
+ lines.append(f"Confidence: {data['confidence']}")
40
+ if data.get("_message"):
41
+ lines.append(f"\n{data['_message']}")
42
+
43
+ return "\n".join(lines)
@@ -0,0 +1,49 @@
1
+ """Meta MCP tools — server info, capabilities, city listing.
2
+
3
+ Discovery endpoints so callers can learn which cities and services are
4
+ available before making domain-specific calls.
5
+ """
6
+
7
+ import json
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+ from clevertech_mcp.client import CleverTechClient
11
+
12
+
13
+ def register_meta_tools(mcp: FastMCP, client: CleverTechClient, config: dict) -> None:
14
+ """Register meta-discovery tools on the FastMCP server."""
15
+
16
+ @mcp.tool(
17
+ name="list_cities",
18
+ description=(
19
+ "List all available cities with their slugs, API capabilities, "
20
+ "and property counts. Use this first to discover which cities are "
21
+ "available and what services each supports."
22
+ ),
23
+ )
24
+ async def list_cities() -> str:
25
+ """List available cities and their capabilities."""
26
+ data = await client.get("/api/cities")
27
+
28
+ cities = data.get("cities", data.get("results", []))
29
+ if not cities:
30
+ return json.dumps(data, indent=2, default=str)
31
+
32
+ lines: list[str] = ["# Available Cities\n"]
33
+ for city in cities:
34
+ name = city.get("name", city.get("city", "Unknown"))
35
+ slug = city.get("slug", city.get("city_slug", "?"))
36
+ props = city.get("property_count", city.get("count", "?"))
37
+ services = city.get("services", city.get("capabilities", []))
38
+
39
+ lines.append(f"## {name}")
40
+ lines.append(f"- Slug: {slug}")
41
+ if isinstance(props, int):
42
+ lines.append(f"- Properties: {props:,}")
43
+ else:
44
+ lines.append(f"- Properties: {props}")
45
+ if services:
46
+ lines.append(f"- Services: {', '.join(services)}")
47
+ lines.append("")
48
+
49
+ return "\n".join(lines)
@@ -0,0 +1,167 @@
1
+ """Property-related MCP tools.
2
+
3
+ Search, lookup, and report on properties across Canadian cities.
4
+ Provides assessment data, building permits, zoning, and DLS coordinates.
5
+ """
6
+
7
+ import json
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+ from clevertech_mcp.client import CleverTechClient
11
+
12
+
13
+ def register_property_tools(
14
+ mcp: FastMCP, client: CleverTechClient, config: dict
15
+ ) -> None:
16
+ """Register property search, by-roll, and report tools."""
17
+
18
+ @mcp.tool(
19
+ name="property_search",
20
+ description=(
21
+ "Search for properties by address across 13+ Canadian cities. "
22
+ "Returns assessment data including value, lot size, year built, "
23
+ "and DLS coordinates."
24
+ ),
25
+ )
26
+ async def property_search(
27
+ city: str,
28
+ address: str,
29
+ limit: int = 10,
30
+ offset: int = 0,
31
+ ) -> str:
32
+ """Search for properties by address.
33
+
34
+ Args:
35
+ city: City slug (calgary, toronto, vancouver, montreal, edmonton,
36
+ etc.).
37
+ address: Street address to search (partial matches supported).
38
+ limit: Max results (1-200, default 10).
39
+ offset: Pagination offset.
40
+ """
41
+ data = await client.get(
42
+ f"/api/{city}/property/search",
43
+ params={
44
+ "address": address,
45
+ "limit": min(limit, 200),
46
+ "offset": offset,
47
+ },
48
+ )
49
+
50
+ results = data.get("results", [])
51
+ total = data.get("total", 0)
52
+ message = data.get("_message", "")
53
+
54
+ lines: list[str] = [f"Found {len(results)} of {total} properties"]
55
+ if message:
56
+ lines.append(f"\n{message}")
57
+
58
+ for r in results:
59
+ lines.extend(
60
+ [
61
+ "",
62
+ "---",
63
+ f"Address: {r.get('address', 'N/A')}",
64
+ f"Roll: {r.get('roll_number', 'N/A')}",
65
+ ]
66
+ )
67
+ if r.get("assessed_value") is not None:
68
+ lines.append(f"Assessed Value: ${r['assessed_value']:,.0f}")
69
+ else:
70
+ lines.append("Assessed Value: N/A")
71
+ lines.append(f"Type: {r.get('property_type', 'N/A')}")
72
+ if r.get("dls"):
73
+ lines.append(f"DLS: {r['dls']}")
74
+
75
+ return "\n".join(lines)
76
+
77
+ @mcp.tool(
78
+ name="property_report",
79
+ description=(
80
+ "Get a consolidated property report — assessment data, building "
81
+ "permits at this address, zoning information, and DLS coordinates "
82
+ "in a single response."
83
+ ),
84
+ )
85
+ async def property_report(city: str, roll_number: str) -> str:
86
+ """Get a consolidated property report.
87
+
88
+ Args:
89
+ city: City slug.
90
+ roll_number: Property roll number.
91
+ """
92
+ data = await client.get(f"/api/{city}/property/report/{roll_number}")
93
+
94
+ message = data.get("_message", "")
95
+ lines: list[str] = [
96
+ "# Property Report",
97
+ f"**Address:** {data.get('address', 'N/A')}",
98
+ f"**Roll:** {data.get('roll_number', 'N/A')}",
99
+ f"**DLS:** {data.get('dls', 'N/A')}",
100
+ f"**Generated:** {data.get('generated_at', 'N/A')}",
101
+ ]
102
+
103
+ prop = data.get("property", {})
104
+ if prop:
105
+ lines.append("")
106
+ lines.append("## Assessment")
107
+ if prop.get("assessed_value") is not None:
108
+ lines.append(f"- Assessed Value: ${prop['assessed_value']:,.0f}")
109
+ else:
110
+ lines.append("- Assessed Value: N/A")
111
+ if prop.get("land_value") is not None:
112
+ lines.append(f"- Land: ${prop['land_value']:,.0f}")
113
+ else:
114
+ lines.append("- Land: N/A")
115
+ if prop.get("improvement_value") is not None:
116
+ lines.append(f"- Building: ${prop['improvement_value']:,.0f}")
117
+ else:
118
+ lines.append("- Building: N/A")
119
+ lines.append(f"- Year Built: {prop.get('year_built', 'N/A')}")
120
+ lines.append(f"- Lot Size: {prop.get('lot_size_sqm', 'N/A')} m²")
121
+
122
+ permits = data.get("permits", [])
123
+ if permits:
124
+ lines.append(f"\n## Building Permits ({len(permits)})")
125
+ for p in permits[:10]:
126
+ job_value = (
127
+ f"${p.get('job_value', 0):,.0f}"
128
+ if p.get("job_value") is not None
129
+ else "N/A"
130
+ )
131
+ lines.append(
132
+ f"- {p.get('permit_number', 'N/A')}: "
133
+ f"{p.get('permit_type', 'N/A')} — {job_value}"
134
+ )
135
+
136
+ zoning = data.get("zoning")
137
+ if zoning:
138
+ lines.extend(
139
+ [
140
+ "",
141
+ "## Zoning",
142
+ f"- Zone: {zoning.get('zone_code', 'N/A')}",
143
+ f"- Description: {zoning.get('description', 'N/A')}",
144
+ ]
145
+ )
146
+
147
+ if message:
148
+ lines.append(f"\n{message}")
149
+
150
+ return "\n".join(lines)
151
+
152
+ @mcp.tool(
153
+ name="property_by_roll",
154
+ description=(
155
+ "Get a single property assessment record by its roll number. "
156
+ "Faster than search when you already know the roll number."
157
+ ),
158
+ )
159
+ async def property_by_roll(city: str, roll_number: str) -> str:
160
+ """Get a property by its roll number.
161
+
162
+ Args:
163
+ city: City slug.
164
+ roll_number: Property roll number.
165
+ """
166
+ data = await client.get(f"/api/{city}/property/by-roll/{roll_number}")
167
+ return json.dumps(data, indent=2, default=str)
@@ -0,0 +1,58 @@
1
+ """Zoning MCP tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+ from clevertech_mcp.client import CleverTechClient
5
+
6
+
7
+ def register_zoning_tools(mcp: FastMCP, client: CleverTechClient, config: dict):
8
+ """Register zoning lookup tools."""
9
+
10
+ @mcp.tool(
11
+ name="zoning_lookup",
12
+ description="Look up zoning district information for a GPS point or address in a Canadian city. Returns zone code, description, and district boundaries.",
13
+ )
14
+ async def zoning_lookup(
15
+ city: str,
16
+ lat: float = None,
17
+ lon: float = None,
18
+ address: str = None,
19
+ ) -> str:
20
+ """Get zoning information.
21
+
22
+ Args:
23
+ city: City slug
24
+ lat: Latitude (alternative to address)
25
+ lon: Longitude (alternative to address)
26
+ address: Street address (alternative to lat/lon)
27
+ """
28
+ if lat is not None and lon is not None:
29
+ params = {"lat": lat, "lon": lon}
30
+ data = await client.get(f"/api/{city}/zoning/by-gps", params=params)
31
+ elif address:
32
+ data = await client.get(f"/api/{city}/zoning/by-gps", params={"address": address})
33
+ else:
34
+ return "Error: Provide either lat+lon or address."
35
+
36
+ message = data.get("_message", "")
37
+ zoning = data.get("zoning", data)
38
+ if isinstance(zoning, list):
39
+ zoning = zoning[0] if zoning else {}
40
+
41
+ lines = [
42
+ f"Zone: {zoning.get('zone_code', zoning.get('zone', 'N/A'))}",
43
+ f"Description: {zoning.get('description', 'N/A')}",
44
+ ]
45
+
46
+ if zoning.get("district_name"):
47
+ lines.append(f"District: {zoning['district_name']}")
48
+ if zoning.get("land_use"):
49
+ lines.append(f"Land Use: {zoning['land_use']}")
50
+
51
+ bounds = zoning.get("boundaries", zoning.get("geometry"))
52
+ if bounds:
53
+ lines.append(f"Boundaries: Available ({type(bounds).__name__})")
54
+
55
+ if message:
56
+ lines.append(f"\n{message}")
57
+
58
+ return "\n".join(lines)
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: clevertech-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP Server for CleverTech Canadian government data APIs
5
+ Author-email: CleverTech <dev@clevertech.ca>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: canada,dls,gis,land-survey,mcp,property
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: httpx>=0.27.0
16
+ Requires-Dist: mcp>=1.0.0
17
+ Requires-Dist: pydantic>=2.0.0
18
+ Requires-Dist: python-dotenv>=1.0.0
19
+ Requires-Dist: starlette>=0.36.0
20
+ Requires-Dist: uvicorn>=0.27.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # CleverTech MCP Server
24
+
25
+ **Canadian government data for AI agents.** Property assessments, building permits, zoning, business registry, DLS grid conversion — 50 free queries/day, no signup.
26
+
27
+ ```bash
28
+ npx @clevertech/mcp-server
29
+ ```
30
+
31
+ [![PyPI version](https://badge.fury.io/py/clevertech-mcp-server.svg)](https://pypi.org/project/clevertech-mcp-server/)
32
+ [![npm version](https://badge.fury.io/npm/%40clevertech%2Fmcp-server.svg)](https://www.npmjs.com/package/@clevertech/mcp-server)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ### Claude Code
40
+
41
+ Add to your `~/.claude/claude_desktop_config.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "clevertech": {
47
+ "command": "npx",
48
+ "args": ["-y", "@clevertech/mcp-server"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Cursor / Cline
55
+
56
+ Add to your MCP configuration:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "clevertech": {
62
+ "command": "uvx",
63
+ "args": ["clevertech-mcp-server"]
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### GitHub Copilot
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "clevertech": {
75
+ "command": "npx",
76
+ "args": ["-y", "@clevertech/mcp-server"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### Direct CLI
83
+
84
+ ```bash
85
+ # npm (Node.js)
86
+ npx @clevertech/mcp-server
87
+
88
+ # uv / PyPI
89
+ uvx clevertech-mcp-server
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Available Tools (11)
95
+
96
+ | Tool | Description |
97
+ |------|-------------|
98
+ | `dls_convert` | GPS ↔ Dominion Land Survey (DLS) coordinate conversion for Western Canada |
99
+ | `dls_batch` | Batch GPS ↔ DLS conversion — up to 100 coordinates at once |
100
+ | `property_search` | Property assessment search by address across 13+ Canadian cities |
101
+ | `property_report` | Consolidated property report — assessment, permits, zoning, and DLS in one call |
102
+ | `property_by_roll` | Direct property lookup by roll number — faster than address search |
103
+ | `building_permit_search` | Building permit search by address, contractor, applicant, or permit number |
104
+ | `building_permit_recent` | Recently issued building permits feed — monitor new construction activity |
105
+ | `zoning_lookup` | Zoning district lookup by GPS coordinates or street address |
106
+ | `business_registry_search` | Federal corporation search — find Canadian businesses by name, province, or city |
107
+ | `reverse_geocode` | GPS → address, city, neighborhood, province, and DLS grid reference |
108
+ | `list_cities` | Discover available cities, their API capabilities, and property counts |
109
+
110
+ ### What Agents Can Do
111
+
112
+ - **Ask**: *"What's 1532 14 Ave SW in Calgary worth?"* — Gets assessed value, land value, building value, year built, lot size, and DLS coordinates.
113
+ - **Ask**: *"Show me recent building permits in Vancouver"* — Returns the latest construction permits with type, value, status, and issue dates.
114
+ - **Ask**: *"What zone is this GPS point in?"* — Looks up zoning district, land use, and boundary info.
115
+ - **Ask**: *"Convert these 50 GPS points to DLS"* — Batch converts in a single API call.
116
+ - **Ask**: *"Is Shopify still an active corporation?"* — Searches the federal business registry.
117
+
118
+ ---
119
+
120
+ ## Cities Supported
121
+
122
+ **20 cities** across Canada — and growing:
123
+
124
+ | Province | Cities |
125
+ |----------|--------|
126
+ | Alberta | Calgary, Edmonton, High River |
127
+ | British Columbia | Vancouver, Victoria |
128
+ | Ontario | Toronto, Ottawa, Hamilton, Kitchener, London, Mississauga, Markham, Vaughan |
129
+ | Quebec | Montreal, Quebec City |
130
+ | Manitoba | Winnipeg |
131
+ | Saskatchewan | Saskatoon, Regina |
132
+ | Nova Scotia | Halifax |
133
+ | Newfoundland | St. John's |
134
+
135
+ Use `list_cities` to see live availability, API capabilities per city, and property counts.
136
+
137
+ ---
138
+
139
+ ## Pricing
140
+
141
+ No signup required to start. Free tier gives you 50 queries/day out of the box.
142
+
143
+ | Tier | Queries/Day | Price | API Key |
144
+ |------|-------------|-------|---------|
145
+ | **Free** | 50 | $0 | Not required |
146
+ | **Developer** | 200 | Free | [Sign up →](https://clevertech.ca/keys) |
147
+ | **Pro** | 2,000 | $10/mo | [Sign up →](https://clevertech.ca/keys) |
148
+ | **Enterprise** | 10,000 | $40/mo | [Sign up →](https://clevertech.ca/keys) |
149
+
150
+ [Get your API key →](https://clevertech.ca/keys)
151
+
152
+ ### Using an API Key
153
+
154
+ ```bash
155
+ npx @clevertech/mcp-server --api-key YOUR_KEY
156
+ ```
157
+
158
+ Or via environment variable:
159
+
160
+ ```bash
161
+ export CLEVERTECH_API_KEY=your_key_here
162
+ npx @clevertech/mcp-server
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Features
168
+
169
+ - **No signup required** — 50 free queries/day out of the box
170
+ - **11 MCP tools** covering property, permits, zoning, business registry, and DLS
171
+ - **Batch DLS conversion** — convert up to 100 coordinates in one call
172
+ - **Consolidated property reports** — assessment + permits + zoning + DLS in a single response
173
+ - **stdio + SSE transport** — works with Claude Code, Cursor, Cline, GitHub Copilot, and any MCP-compatible agent
174
+ - **Clean text output** — formatted for easy LLM parsing and reformatting
175
+ - **20 Canadian cities** — urban and rural coverage with regular additions
176
+ - **MIT licensed** — use it anywhere, no restrictions
177
+
178
+ ---
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ # Clone and set up
184
+ git clone https://github.com/anteriorclever/clevertech-mcp-server.git
185
+ cd clevertech-mcp-server
186
+ uv sync
187
+
188
+ # Run locally (stdio transport)
189
+ uv run clevertech-mcp-server --transport stdio
190
+
191
+ # Run with SSE transport (for HTTP-based agents)
192
+ uv run clevertech-mcp-server --transport sse --port 8000
193
+ ```
194
+
195
+ ### Run Tests
196
+
197
+ ```bash
198
+ uv run pytest
199
+ ```
200
+
201
+ ### Project Structure
202
+
203
+ ```
204
+ src/clevertech_mcp/
205
+ ├── tools/
206
+ │ ├── property.py # property_search, property_report, property_by_roll
207
+ │ ├── building.py # building_permit_search, building_permit_recent
208
+ │ ├── zoning.py # zoning_lookup
209
+ │ ├── dls.py # dls_convert, dls_batch
210
+ │ ├── business.py # business_registry_search
211
+ │ ├── geo.py # reverse_geocode
212
+ │ └── meta.py # list_cities
213
+ ├── server.py # FastMCP server entry point
214
+ ├── client.py # HTTP client for CleverTech API
215
+ ├── auth.py # API key authentication
216
+ ├── rate_limit.py # Rate limiting
217
+ └── config.py # Configuration management
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Documentation
223
+
224
+ - [Tool Reference](docs/tools.md)
225
+ - [MCP Protocol Specification](https://modelcontextprotocol.io)
226
+ - [CleverTech API Keys](https://clevertech.ca/keys)
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ MIT © 2026 CleverTech — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,19 @@
1
+ clevertech_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ clevertech_mcp/auth.py,sha256=xgmWMnBfqB5Mf2AbAeM4L1G0URj2rYT29UCiH8UFyFA,2247
3
+ clevertech_mcp/client.py,sha256=THID5xn-1yCC6dDIK9GZab94Q37Qfw1K-wZEQzNOCGw,1823
4
+ clevertech_mcp/config.py,sha256=XOnv9AZxSfIhDo16k8CSES0_TmNXXqu3Jdiqvf6rI-Q,1331
5
+ clevertech_mcp/rate_limit.py,sha256=9qmAqNwK9hprNpvTAYlI8BIBehhuaygoYojM5WKxmPg,3567
6
+ clevertech_mcp/server.py,sha256=JhRpKOT5ImJauzpW8mf6CGk8t8MRgpsDBsw0qt3esCQ,2280
7
+ clevertech_mcp/tools/__init__.py,sha256=oC66yJC3AwG7VFsFW7mWLNlIJ-frPh3FzmwKCVL9GeE,1624
8
+ clevertech_mcp/tools/building.py,sha256=fHToQxUsZbiS1xlXyPwbTo0SCh2qcUY-VY_TpOqsjYg,3331
9
+ clevertech_mcp/tools/business.py,sha256=dPESy1X7fotYVr8zyzpt2_5FHlQ5JjlQYtcloVrvLwc,2069
10
+ clevertech_mcp/tools/dls.py,sha256=RaqxRy3zLfAz5n4hQUm2rvpCy5a6HWS2vMvBbJicXJc,4356
11
+ clevertech_mcp/tools/geo.py,sha256=8BNg8HH2R37XKZoLKqg_TChmzGXs3djrwdN4zVOvHss,1555
12
+ clevertech_mcp/tools/meta.py,sha256=K9WeOmYPoQpqEvw1KQ8bJa8r0eyiP8vJM_sON1HAz9w,1788
13
+ clevertech_mcp/tools/property.py,sha256=r9vYZBvKUN90xf4PQldgJ_9pJNj-a4ZdeEH6zXr2upI,5677
14
+ clevertech_mcp/tools/zoning.py,sha256=rwKYt1BhCgHRR18th2hhY8vI2koIG6rb05-cOscNmHA,2020
15
+ clevertech_mcp_server-0.1.0.dist-info/METADATA,sha256=z-lxFnN2LGXQYZq0yi93MnI1t-mKZFW27nBpgG8oThU,7039
16
+ clevertech_mcp_server-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ clevertech_mcp_server-0.1.0.dist-info/entry_points.txt,sha256=5DBsT1PeZ75Kwp2MCi6RFU4_704NTddkpXqyO85hWyo,69
18
+ clevertech_mcp_server-0.1.0.dist-info/licenses/LICENSE,sha256=nm-6Sfd2weKWBQsLq4qTjKEzdAKVIA4Y11gUniTqcIg,1067
19
+ clevertech_mcp_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ clevertech-mcp-server = clevertech_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CleverTech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.