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.
- ecs_mcp-0.1.0/.claude/settings.local.json +8 -0
- ecs_mcp-0.1.0/PKG-INFO +7 -0
- ecs_mcp-0.1.0/mcp.specification +35 -0
- ecs_mcp-0.1.0/pyproject.toml +19 -0
- ecs_mcp-0.1.0/src/ecs_mcp/__init__.py +6 -0
- ecs_mcp-0.1.0/src/ecs_mcp/__main__.py +3 -0
- ecs_mcp-0.1.0/src/ecs_mcp/server.py +12 -0
- ecs_mcp-0.1.0/src/ecs_mcp/tools/__init__.py +6 -0
- ecs_mcp-0.1.0/src/ecs_mcp/tools/affinity.py +115 -0
- ecs_mcp-0.1.0/templates-claude.prompts +8 -0
ecs_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -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,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.
|