ecs-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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:github.com)",
5
+ "Bash(uvx --help)"
6
+ ]
7
+ }
8
+ }
ecs_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: ecs-mcp
3
+ Version: 0.1.0
4
+ Summary: ECS MCP server
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: mcp[cli]
@@ -0,0 +1,35 @@
1
+ # ECS-MCP
2
+ This is an MCP server tool for the ECS company.
3
+
4
+ ## Requirements
5
+ Must be Claude Desktop compatible with the ability to be configure via `claude_desktop_config.json` like below.
6
+ ```
7
+ {
8
+ "mcpServers": {
9
+ "ecs-mcp": {
10
+ "command": "your-uvx-path-here",
11
+ "args": ["ecs-mcp"],
12
+ "env": {
13
+ "AFFINITY_API_KEY": "your_api_key_here"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Prerequisites:
21
+ * An Affinity account with API access (Scale, Advanced, or Enterprise)
22
+ * UV Python package manager
23
+ * An MCP-compatible AI assistant (e.g., Claude Desktop, GitHub Copilot, Gemini CLI)
24
+ * An Affinity API key — see the Authentication page for help obtaining one
25
+
26
+ ## Tools
27
+ ### Affinity
28
+ Create Affinity CRM tool that is a facade of its API:
29
+ https://api-docs.affinity.co/?utm_source=chatgpt.com#introduction-to-api-v1
30
+
31
+ Only create functions in the Affinity tool listed below.
32
+ Do not create any additional ones that are not listed below.
33
+
34
+ #### Create a new deal
35
+ Create a new deal on an existing list that's provided as a parameter (e.g. "ALL DEALS ECS").
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "ecs-mcp"
3
+ version = "0.1.0"
4
+ description = "ECS MCP server"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "mcp[cli]",
8
+ "httpx>=0.27",
9
+ ]
10
+
11
+ [project.scripts]
12
+ ecs-mcp = "ecs_mcp.server:main"
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/ecs_mcp"]
@@ -0,0 +1,6 @@
1
+ """ECS MCP Server — Affinity CRM integration."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ # Shared FastMCP instance — imported by server.py and all tool modules
6
+ mcp = FastMCP("ecs-mcp")
@@ -0,0 +1,3 @@
1
+ from ecs_mcp.server import main
2
+
3
+ main()
@@ -0,0 +1,12 @@
1
+ """ECS MCP Server bootstrap."""
2
+
3
+ from ecs_mcp import mcp
4
+ import ecs_mcp.tools # noqa: F401 — registers all tool modules as a side effect
5
+
6
+
7
+ def main() -> None:
8
+ mcp.run(transport="stdio")
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,6 @@
1
+ """Tool registry — import each tool module here to register its @mcp.tool() decorators."""
2
+
3
+ from ecs_mcp.tools import affinity # noqa: F401
4
+
5
+ # To add a new integration, add an import here:
6
+ # from ecs_mcp.tools import hubspot # noqa: F401
@@ -0,0 +1,115 @@
1
+ """Affinity CRM tools for ecs-mcp."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from ecs_mcp import mcp
9
+
10
+ AFFINITY_BASE_URL = "https://api.affinity.co"
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Affinity API client helpers
15
+ # ---------------------------------------------------------------------------
16
+
17
+ def _get_api_key() -> str:
18
+ key = os.environ.get("AFFINITY_API_KEY", "").strip()
19
+ if not key:
20
+ raise ValueError(
21
+ "AFFINITY_API_KEY environment variable is not set. "
22
+ "Add it to your Claude Desktop config under 'env'."
23
+ )
24
+ return key
25
+
26
+
27
+ def _client() -> httpx.AsyncClient:
28
+ return httpx.AsyncClient(
29
+ base_url=AFFINITY_BASE_URL,
30
+ auth=("", _get_api_key()),
31
+ headers={"Content-Type": "application/json"},
32
+ timeout=30.0,
33
+ )
34
+
35
+
36
+ async def _find_list_by_name(client: httpx.AsyncClient, list_name: str) -> dict[str, Any] | None:
37
+ response = await client.get("/lists")
38
+ response.raise_for_status()
39
+ for lst in response.json():
40
+ if lst.get("name", "").lower() == list_name.lower():
41
+ return lst
42
+ return None
43
+
44
+
45
+ async def _find_organization(client: httpx.AsyncClient, org_name: str) -> dict[str, Any] | None:
46
+ response = await client.get("/organizations", params={"term": org_name})
47
+ response.raise_for_status()
48
+ orgs = response.json().get("organizations", [])
49
+ if not orgs:
50
+ return None
51
+ for org in orgs:
52
+ if org.get("name", "").lower() == org_name.lower():
53
+ return org
54
+ return orgs[0]
55
+
56
+
57
+ async def _create_organization(client: httpx.AsyncClient, org_name: str) -> dict[str, Any]:
58
+ slug = "".join(c for c in org_name.lower() if c.isalnum())
59
+ response = await client.post(
60
+ "/organizations",
61
+ json={"name": org_name, "domain": f"{slug}.com"},
62
+ )
63
+ response.raise_for_status()
64
+ return response.json()
65
+
66
+
67
+ async def _create_list_entry(
68
+ client: httpx.AsyncClient, list_id: int, entity_id: int
69
+ ) -> dict[str, Any]:
70
+ response = await client.post(
71
+ f"/lists/{list_id}/list-entries",
72
+ json={"entity_id": entity_id},
73
+ )
74
+ response.raise_for_status()
75
+ return response.json()
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # MCP tool
80
+ # ---------------------------------------------------------------------------
81
+
82
+ @mcp.tool()
83
+ async def create_deal(list_name: str, organization_name: str) -> str:
84
+ """Add a company as a new deal entry on an Affinity CRM list.
85
+
86
+ Finds or creates the organization in Affinity, then adds it to the
87
+ specified list. Avoids creating duplicate organizations.
88
+
89
+ Args:
90
+ list_name: The exact name of the Affinity list (e.g. "ALL DEALS ECS").
91
+ organization_name: The company name to add (e.g. "Acme Corp").
92
+ """
93
+ async with _client() as client:
94
+ lst = await _find_list_by_name(client, list_name)
95
+ if lst is None:
96
+ return (
97
+ f"Error: No Affinity list found with name '{list_name}'. "
98
+ "Check the list name in Affinity and try again."
99
+ )
100
+
101
+ org = await _find_organization(client, organization_name)
102
+ created_new = org is None
103
+ if created_new:
104
+ org = await _create_organization(client, organization_name)
105
+
106
+ org_id: int = org["id"]
107
+ entry = await _create_list_entry(client, lst["id"], org_id)
108
+
109
+ org_status = "created new organization" if created_new else "found existing organization"
110
+ return (
111
+ f"Successfully added '{org.get('name', organization_name)}' to '{lst['name']}'.\n"
112
+ f" - Organization: {org_status} (id={org_id})\n"
113
+ f" - List entry id: {entry.get('id', '?')}\n"
114
+ f" - List: '{lst['name']}' (id={lst['id']})"
115
+ )
@@ -0,0 +1,8 @@
1
+ # Prompt templates for Claude
2
+
3
+ ## New opportunities in Gmail
4
+ Discover new opportunity companies from my emails in Gmail and add them to Affinity CRM.
5
+ Emails that contain new opporutnity companies are labeled "opportunity".
6
+ Add the new opportunities in the "ALL DEALS ECS" list in Affinity.
7
+ Emails that have already been added to the Affinity CRM are labeled "in_affinity" under "automation".
8
+ Do not add duplicate entry.