finpy-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.
- finpy_mcp-0.1.0/PKG-INFO +12 -0
- finpy_mcp-0.1.0/README.md +0 -0
- finpy_mcp-0.1.0/pyproject.toml +22 -0
- finpy_mcp-0.1.0/src/finpy_mcp/__init__.py +2 -0
- finpy_mcp-0.1.0/src/finpy_mcp/cli.py +191 -0
- finpy_mcp-0.1.0/src/finpy_mcp/client.py +122 -0
- finpy_mcp-0.1.0/src/finpy_mcp/config.py +17 -0
- finpy_mcp-0.1.0/src/finpy_mcp/credentials.py +50 -0
- finpy_mcp-0.1.0/src/finpy_mcp/server.py +35 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/__init__.py +0 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/captable.py +377 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/context.py +92 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/deals.py +208 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/entity.py +74 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/financials.py +186 -0
- finpy_mcp-0.1.0/src/finpy_mcp/tools/holdings.py +129 -0
finpy_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: finpy-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Finpy MCP Server — AI-powered asset management through conversation
|
|
5
|
+
Author: Robert Radoslav
|
|
6
|
+
Author-email: Robert Radoslav <43938206+rbtrsv@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: click>=8.3.2
|
|
8
|
+
Requires-Dist: fastmcp>=3.2.3
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "finpy-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Finpy MCP Server — AI-powered asset management through conversation"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Robert Radoslav", email = "43938206+rbtrsv@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click>=8.3.2",
|
|
12
|
+
"fastmcp>=3.2.3",
|
|
13
|
+
"httpx>=0.28.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
finpy-mcp = "finpy_mcp.server:main"
|
|
18
|
+
finpy-cli = "finpy_mcp.cli:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.8.15,<0.9.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finpy CLI — Interactive terminal authentication
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
finpy-cli login — Login via browser (OAuth device flow)
|
|
6
|
+
finpy-cli login --basic — Login with email/password in terminal (fallback)
|
|
7
|
+
finpy-cli logout — Delete stored credentials
|
|
8
|
+
finpy-cli set-org — Set active organization
|
|
9
|
+
finpy-cli status — Show current auth status
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import getpass
|
|
13
|
+
import time
|
|
14
|
+
import webbrowser
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .config import API_BASE_URL, REQUEST_TIMEOUT
|
|
20
|
+
from .credentials import load_credentials, save_credentials, delete_credentials
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
def main():
|
|
25
|
+
"""Finpy CLI — Authenticate and manage your Finpy MCP connection."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _save_and_show(data: dict) -> None:
|
|
30
|
+
"""Save credentials and show orgs. Shared between login flows."""
|
|
31
|
+
token = data["token"]
|
|
32
|
+
user = data["data"]["user"]
|
|
33
|
+
orgs = data["data"]["organizations"]
|
|
34
|
+
|
|
35
|
+
# Default to first organization
|
|
36
|
+
default_org_id = orgs[0]["id"] if orgs else None
|
|
37
|
+
|
|
38
|
+
save_credentials({
|
|
39
|
+
"access_token": token["access_token"],
|
|
40
|
+
"refresh_token": token["refresh_token"],
|
|
41
|
+
"organization_id": default_org_id,
|
|
42
|
+
"user_email": user["email"],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
click.echo(f"\n✓ Logged in as {user['email']}")
|
|
46
|
+
click.echo(f"✓ Credentials saved to ~/.finpy/credentials.json\n")
|
|
47
|
+
|
|
48
|
+
if orgs:
|
|
49
|
+
click.echo("Organizations:")
|
|
50
|
+
for org in orgs:
|
|
51
|
+
marker = " (active)" if org["id"] == default_org_id else ""
|
|
52
|
+
click.echo(f" {org['id']}. {org['name']} — role: {org['user_role']}{marker}")
|
|
53
|
+
click.echo(f"\nUse 'finpy-cli set-org <id>' to change active organization.")
|
|
54
|
+
else:
|
|
55
|
+
click.echo("Warning: No organizations found. Create one in the Finpy dashboard.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@main.command()
|
|
59
|
+
@click.option("--basic", is_flag=True, help="Use email/password login instead of browser (for headless environments)")
|
|
60
|
+
def login(basic: bool):
|
|
61
|
+
"""Login to Finpy. Opens browser by default (OAuth device flow).
|
|
62
|
+
Use --basic for email/password in terminal."""
|
|
63
|
+
if basic:
|
|
64
|
+
_login_basic()
|
|
65
|
+
else:
|
|
66
|
+
_login_device()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _login_device():
|
|
70
|
+
"""Device flow: open browser, user approves, CLI polls for token."""
|
|
71
|
+
try:
|
|
72
|
+
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
|
|
73
|
+
# Step 1: Request device code
|
|
74
|
+
r = client.post(f"{API_BASE_URL}/accounts/auth/device-code")
|
|
75
|
+
r.raise_for_status()
|
|
76
|
+
device = r.json()
|
|
77
|
+
|
|
78
|
+
device_code = device["device_code"]
|
|
79
|
+
user_code = device["user_code"]
|
|
80
|
+
verification_url = device["verification_url"]
|
|
81
|
+
interval = device.get("interval", 2)
|
|
82
|
+
expires_in = device.get("expires_in", 300)
|
|
83
|
+
|
|
84
|
+
# Step 2: Open browser
|
|
85
|
+
click.echo(f"\nOpening browser to authorize...")
|
|
86
|
+
click.echo(f"If browser doesn't open, go to: {verification_url}")
|
|
87
|
+
click.echo(f"Code: {user_code}\n")
|
|
88
|
+
webbrowser.open(verification_url)
|
|
89
|
+
|
|
90
|
+
# Step 3: Poll for token
|
|
91
|
+
click.echo("Waiting for authorization", nl=False)
|
|
92
|
+
start = time.time()
|
|
93
|
+
while time.time() - start < expires_in:
|
|
94
|
+
time.sleep(interval)
|
|
95
|
+
click.echo(".", nl=False)
|
|
96
|
+
|
|
97
|
+
r = client.post(
|
|
98
|
+
f"{API_BASE_URL}/accounts/auth/device-token",
|
|
99
|
+
json={"device_code": device_code},
|
|
100
|
+
)
|
|
101
|
+
data = r.json()
|
|
102
|
+
|
|
103
|
+
if data.get("success"):
|
|
104
|
+
click.echo("") # newline after dots
|
|
105
|
+
_save_and_show(data)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
error = data.get("error", "")
|
|
109
|
+
if error == "authorization_pending":
|
|
110
|
+
continue
|
|
111
|
+
elif error == "expired_token":
|
|
112
|
+
click.echo("\nError: Code expired. Please try again.", err=True)
|
|
113
|
+
raise SystemExit(1)
|
|
114
|
+
elif error == "access_denied":
|
|
115
|
+
click.echo("\nError: Authorization denied.", err=True)
|
|
116
|
+
raise SystemExit(1)
|
|
117
|
+
else:
|
|
118
|
+
click.echo(f"\nError: {error}", err=True)
|
|
119
|
+
raise SystemExit(1)
|
|
120
|
+
|
|
121
|
+
click.echo("\nError: Timed out waiting for authorization.", err=True)
|
|
122
|
+
raise SystemExit(1)
|
|
123
|
+
|
|
124
|
+
except httpx.ConnectError:
|
|
125
|
+
click.echo(f"Error: Cannot connect to {API_BASE_URL}. Is the server running?", err=True)
|
|
126
|
+
raise SystemExit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _login_basic():
|
|
130
|
+
"""Basic flow: email/password in terminal (fallback for headless environments)."""
|
|
131
|
+
email = click.prompt("Email")
|
|
132
|
+
password = getpass.getpass("Password: ")
|
|
133
|
+
|
|
134
|
+
click.echo("Logging in...")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
|
|
138
|
+
r = client.post(
|
|
139
|
+
f"{API_BASE_URL}/accounts/auth/login",
|
|
140
|
+
json={"email": email, "password": password},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if r.status_code == 401:
|
|
144
|
+
click.echo("Error: Invalid email or password.", err=True)
|
|
145
|
+
raise SystemExit(1)
|
|
146
|
+
|
|
147
|
+
r.raise_for_status()
|
|
148
|
+
_save_and_show(r.json())
|
|
149
|
+
|
|
150
|
+
except httpx.HTTPStatusError as e:
|
|
151
|
+
click.echo(f"Error: API returned {e.response.status_code}", err=True)
|
|
152
|
+
raise SystemExit(1)
|
|
153
|
+
except httpx.ConnectError:
|
|
154
|
+
click.echo(f"Error: Cannot connect to {API_BASE_URL}. Is the server running?", err=True)
|
|
155
|
+
raise SystemExit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@main.command()
|
|
159
|
+
def logout():
|
|
160
|
+
"""Delete stored credentials."""
|
|
161
|
+
delete_credentials()
|
|
162
|
+
click.echo("✓ Logged out. Credentials deleted.")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@main.command("set-org")
|
|
166
|
+
@click.argument("org_id", type=int)
|
|
167
|
+
def set_org(org_id: int):
|
|
168
|
+
"""Set the active organization ID."""
|
|
169
|
+
creds = load_credentials()
|
|
170
|
+
if not creds:
|
|
171
|
+
click.echo("Error: Not logged in. Run 'finpy-cli login' first.", err=True)
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
|
|
174
|
+
creds["organization_id"] = org_id
|
|
175
|
+
save_credentials(creds)
|
|
176
|
+
click.echo(f"✓ Active organization set to {org_id}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@main.command()
|
|
180
|
+
def status():
|
|
181
|
+
"""Show current authentication status."""
|
|
182
|
+
creds = load_credentials()
|
|
183
|
+
if not creds:
|
|
184
|
+
click.echo("Not authenticated. Run 'finpy-cli login' first.")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
click.echo(f"Email: {creds.get('user_email', 'unknown')}")
|
|
188
|
+
click.echo(f"Organization ID: {creds.get('organization_id', 'not set')}")
|
|
189
|
+
click.echo(f"API: {API_BASE_URL}")
|
|
190
|
+
click.echo(f"Token: {'present' if creds.get('access_token') else 'missing'}")
|
|
191
|
+
click.echo(f"Refresh token: {'present' if creds.get('refresh_token') else 'missing'}")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finpy MCP Server — HTTP Client
|
|
3
|
+
|
|
4
|
+
Async httpx wrapper that handles:
|
|
5
|
+
- JWT authentication (from ~/.finpy/credentials.json)
|
|
6
|
+
- Organization context (query param)
|
|
7
|
+
- Entity context (for tools that need it)
|
|
8
|
+
- Automatic token refresh on 401 responses
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .config import API_BASE_URL, REQUEST_TIMEOUT
|
|
14
|
+
from .credentials import load_credentials, save_credentials
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FinpyClient:
|
|
18
|
+
"""HTTP client for Finpy API with JWT auth and org/entity context."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.base_url = API_BASE_URL
|
|
22
|
+
self.organization_id: int | None = None
|
|
23
|
+
self.entity_id: int | None = None
|
|
24
|
+
|
|
25
|
+
def _get_credentials(self) -> dict:
|
|
26
|
+
"""Load credentials or raise descriptive error."""
|
|
27
|
+
creds = load_credentials()
|
|
28
|
+
if not creds or not creds.get("access_token"):
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
"Not authenticated. Please run 'finpy-cli login' in your terminal first."
|
|
31
|
+
)
|
|
32
|
+
return creds
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def _headers(self) -> dict:
|
|
36
|
+
"""Auth headers from stored credentials."""
|
|
37
|
+
creds = self._get_credentials()
|
|
38
|
+
return {"Authorization": f"Bearer {creds['access_token']}"}
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def _params(self) -> dict:
|
|
42
|
+
"""Default query params (organization_id)."""
|
|
43
|
+
# Use org from credentials if not overridden
|
|
44
|
+
org_id = self.organization_id
|
|
45
|
+
if org_id is None:
|
|
46
|
+
creds = load_credentials()
|
|
47
|
+
if creds:
|
|
48
|
+
org_id = creds.get("organization_id")
|
|
49
|
+
params: dict = {}
|
|
50
|
+
if org_id is not None:
|
|
51
|
+
params["organization_id"] = org_id
|
|
52
|
+
return params
|
|
53
|
+
|
|
54
|
+
async def _refresh_token(self) -> bool:
|
|
55
|
+
"""Attempt to refresh JWT using refresh_token. Returns True if successful."""
|
|
56
|
+
creds = load_credentials()
|
|
57
|
+
if not creds or not creds.get("refresh_token"):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as http:
|
|
62
|
+
r = await http.post(
|
|
63
|
+
f"{self.base_url}/accounts/auth/refresh-token",
|
|
64
|
+
json={"refresh_token": creds["refresh_token"]},
|
|
65
|
+
)
|
|
66
|
+
if r.status_code == 200:
|
|
67
|
+
data = r.json()
|
|
68
|
+
creds["access_token"] = data["token"]["access_token"]
|
|
69
|
+
creds["refresh_token"] = data["token"]["refresh_token"]
|
|
70
|
+
save_credentials(creds)
|
|
71
|
+
return True
|
|
72
|
+
except httpx.HTTPError:
|
|
73
|
+
pass
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
77
|
+
"""Make HTTP request with auto-refresh on 401."""
|
|
78
|
+
params = {**self._params, **kwargs.pop("params", {})}
|
|
79
|
+
|
|
80
|
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as http:
|
|
81
|
+
r = await http.request(
|
|
82
|
+
method,
|
|
83
|
+
f"{self.base_url}{path}",
|
|
84
|
+
headers=self._headers,
|
|
85
|
+
params=params,
|
|
86
|
+
**kwargs,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Auto-refresh on 401
|
|
90
|
+
if r.status_code == 401:
|
|
91
|
+
refreshed = await self._refresh_token()
|
|
92
|
+
if refreshed:
|
|
93
|
+
r = await http.request(
|
|
94
|
+
method,
|
|
95
|
+
f"{self.base_url}{path}",
|
|
96
|
+
headers=self._headers,
|
|
97
|
+
params=params,
|
|
98
|
+
**kwargs,
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
"Session expired. Please run 'finpy-cli login' again."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
r.raise_for_status()
|
|
106
|
+
return r.json()
|
|
107
|
+
|
|
108
|
+
async def get(self, path: str, **kwargs) -> dict:
|
|
109
|
+
"""GET request to Finpy API."""
|
|
110
|
+
return await self._request("GET", path, **kwargs)
|
|
111
|
+
|
|
112
|
+
async def post(self, path: str, data: dict) -> dict:
|
|
113
|
+
"""POST request to Finpy API."""
|
|
114
|
+
return await self._request("POST", path, json=data)
|
|
115
|
+
|
|
116
|
+
async def put(self, path: str, data: dict) -> dict:
|
|
117
|
+
"""PUT request to Finpy API."""
|
|
118
|
+
return await self._request("PUT", path, json=data)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Singleton instance — shared across all tools
|
|
122
|
+
finpy = FinpyClient()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finpy MCP Server — Configuration
|
|
3
|
+
|
|
4
|
+
API base URL is read from FINPY_API_URL env var.
|
|
5
|
+
Defaults to localhost for development, override for production.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
# API base URL — override with FINPY_API_URL env var for production
|
|
11
|
+
API_BASE_URL = os.environ.get("FINPY_API_URL", "http://localhost:8001")
|
|
12
|
+
|
|
13
|
+
# Credentials file path — where JWT + refresh token are stored locally
|
|
14
|
+
CREDENTIALS_PATH = os.path.expanduser("~/.finpy/credentials.json")
|
|
15
|
+
|
|
16
|
+
# HTTP client timeout (seconds)
|
|
17
|
+
REQUEST_TIMEOUT = 30.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finpy MCP Server — Credentials Management
|
|
3
|
+
|
|
4
|
+
Reads/writes JWT + refresh token to ~/.finpy/credentials.json.
|
|
5
|
+
File permissions set to 600 (owner read/write only).
|
|
6
|
+
Password is NEVER stored — only tokens.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TypedDict
|
|
13
|
+
|
|
14
|
+
from .config import CREDENTIALS_PATH
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Credentials(TypedDict):
|
|
18
|
+
access_token: str
|
|
19
|
+
refresh_token: str
|
|
20
|
+
organization_id: int | None
|
|
21
|
+
user_email: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_credentials() -> Credentials | None:
|
|
25
|
+
"""Load credentials from ~/.finpy/credentials.json. Returns None if not found."""
|
|
26
|
+
path = Path(CREDENTIALS_PATH)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
with open(path) as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, KeyError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def save_credentials(creds: Credentials) -> None:
|
|
37
|
+
"""Save credentials to ~/.finpy/credentials.json with chmod 600."""
|
|
38
|
+
path = Path(CREDENTIALS_PATH)
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
with open(path, "w") as f:
|
|
41
|
+
json.dump(creds, f, indent=2)
|
|
42
|
+
# Owner read/write only — no group/other access
|
|
43
|
+
os.chmod(path, 0o600)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def delete_credentials() -> None:
|
|
47
|
+
"""Delete credentials file (logout)."""
|
|
48
|
+
path = Path(CREDENTIALS_PATH)
|
|
49
|
+
if path.exists():
|
|
50
|
+
path.unlink()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Finpy MCP Server
|
|
3
|
+
|
|
4
|
+
FastMCP instance with all tools registered.
|
|
5
|
+
Entrypoint: finpy-mcp (stdio transport for Claude Desktop/Code/Cursor)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
mcp = FastMCP(
|
|
11
|
+
"Finpy",
|
|
12
|
+
instructions=(
|
|
13
|
+
"Finpy Asset Manager — manage cap tables, deals, holdings, and financials through conversation.\n\n"
|
|
14
|
+
"IMPORTANT: Before using any tools, the user must have run 'finpy-cli login' in their terminal.\n"
|
|
15
|
+
"Then use set_context() to select an organization and entity.\n\n"
|
|
16
|
+
"Workflow: set_context → list/create data.\n"
|
|
17
|
+
"Entity is the foundation — everything (stakeholders, securities, deals, holdings) belongs to an entity.\n"
|
|
18
|
+
"Subscription is charged per entity — inform users when creating new entities."
|
|
19
|
+
),
|
|
20
|
+
mask_error_details=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Register all tools by importing tool modules
|
|
24
|
+
# Each module uses @mcp.tool decorator from this instance
|
|
25
|
+
from .tools import context # noqa: F401, E402
|
|
26
|
+
from .tools import entity # noqa: F401, E402
|
|
27
|
+
from .tools import captable # noqa: F401, E402
|
|
28
|
+
from .tools import deals # noqa: F401, E402
|
|
29
|
+
from .tools import holdings # noqa: F401, E402
|
|
30
|
+
from .tools import financials # noqa: F401, E402
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
"""Entrypoint for finpy-mcp command. Runs stdio transport."""
|
|
35
|
+
mcp.run()
|
|
File without changes
|