adcirc-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.
- adcirc_mcp-0.1.0/.gitignore +45 -0
- adcirc_mcp-0.1.0/LICENSE +21 -0
- adcirc_mcp-0.1.0/PKG-INFO +9 -0
- adcirc_mcp-0.1.0/README.md +65 -0
- adcirc_mcp-0.1.0/pyproject.toml +28 -0
- adcirc_mcp-0.1.0/server.json +22 -0
- adcirc_mcp-0.1.0/smithery.yaml +8 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/__init__.py +0 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/__main__.py +5 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/client.py +118 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/models.py +624 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/server.py +32 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/tools/__init__.py +0 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/tools/docs.py +99 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/tools/parsing.py +288 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/tools/reference.py +167 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/tools/validation.py +181 -0
- adcirc_mcp-0.1.0/src/adcirc_mcp/utils.py +739 -0
- adcirc_mcp-0.1.0/tests/__init__.py +0 -0
- adcirc_mcp-0.1.0/tests/conftest.py +13 -0
- adcirc_mcp-0.1.0/tests/fixtures/fort13_sample.txt +20 -0
- adcirc_mcp-0.1.0/tests/fixtures/fort14_header.txt +7 -0
- adcirc_mcp-0.1.0/tests/fixtures/fort15_errors.txt +34 -0
- adcirc_mcp-0.1.0/tests/fixtures/fort15_minimal.txt +45 -0
- adcirc_mcp-0.1.0/tests/fixtures/fort22_sample.txt +7 -0
- adcirc_mcp-0.1.0/tests/test_live.py +36 -0
- adcirc_mcp-0.1.0/tests/test_mcp_protocol.py +35 -0
- adcirc_mcp-0.1.0/tests/test_parsers.py +222 -0
- adcirc_mcp-0.1.0/tests/test_tools.py +243 -0
- adcirc_mcp-0.1.0/tests/test_validation.py +145 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.whl
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
.tox/
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
*.swo
|
|
27
|
+
*~
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
|
|
33
|
+
# Linting
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
.mypy_cache/
|
|
36
|
+
|
|
37
|
+
# Lock files (each server manages its own)
|
|
38
|
+
uv.lock
|
|
39
|
+
|
|
40
|
+
# Environment
|
|
41
|
+
.env
|
|
42
|
+
.env.local
|
|
43
|
+
|
|
44
|
+
# Registry tokens
|
|
45
|
+
.mcpregistry_*
|
adcirc_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mansur Ali Jisan
|
|
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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adcirc-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for ADCIRC model setup debugging, parameter lookup, and configuration validation
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# adcirc-mcp
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.mansurjisan/adcirc-mcp -->
|
|
4
|
+
|
|
5
|
+
MCP server for ADCIRC model setup debugging, parameter lookup, and configuration validation.
|
|
6
|
+
|
|
7
|
+
Unlike the other ocean-mcp servers that query remote NOAA APIs, adcirc-mcp **parses local model input files** and provides **embedded domain knowledge** to help debug and understand ADCIRC configurations.
|
|
8
|
+
|
|
9
|
+
## Tools (10)
|
|
10
|
+
|
|
11
|
+
### Parameter Reference
|
|
12
|
+
- **`adcirc_explain_parameter`** — Look up any fort.15 parameter, NWS value, tidal constituent, or nodal attribute
|
|
13
|
+
- **`adcirc_list_parameters`** — List all parameters grouped by category
|
|
14
|
+
|
|
15
|
+
### File Parsing
|
|
16
|
+
- **`adcirc_parse_fort15`** — Parse fort.15 control file into structured summary
|
|
17
|
+
- **`adcirc_parse_fort14`** — Parse fort.14 mesh file header (node/element counts, boundaries)
|
|
18
|
+
- **`adcirc_parse_fort13`** — Parse fort.13 nodal attributes (names, defaults, non-default counts)
|
|
19
|
+
- **`adcirc_parse_fort22`** — Parse fort.22 meteorological forcing header
|
|
20
|
+
|
|
21
|
+
### Validation & Debugging
|
|
22
|
+
- **`adcirc_validate_config`** — Comprehensive validation with CFL check and cross-file verification
|
|
23
|
+
- **`adcirc_diagnose_error`** — Match error text against known ADCIRC failure patterns
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
- **`adcirc_fetch_docs`** — Fetch pages from the ADCIRC wiki
|
|
27
|
+
- **`adcirc_search_docs`** — Search the ADCIRC wiki
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Using uvx (recommended)
|
|
33
|
+
uvx adcirc-mcp
|
|
34
|
+
|
|
35
|
+
# Or install from source
|
|
36
|
+
cd servers/adcirc-mcp
|
|
37
|
+
uv sync
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Add to your MCP client config:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"adcirc": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["adcirc-mcp"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd servers/adcirc-mcp
|
|
59
|
+
uv sync --group dev
|
|
60
|
+
uv run pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_mcp_protocol.py -v
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "adcirc-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for ADCIRC model setup debugging, parameter lookup, and configuration validation"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"mcp[cli]>=1.0.0",
|
|
8
|
+
"httpx>=0.27.0",
|
|
9
|
+
"pydantic>=2.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
adcirc-mcp = "adcirc_mcp.server:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.pytest.ini_options]
|
|
20
|
+
testpaths = ["tests"]
|
|
21
|
+
asyncio_mode = "auto"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0.0",
|
|
26
|
+
"pytest-asyncio>=0.24.0",
|
|
27
|
+
"respx>=0.22.0",
|
|
28
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.mansurjisan/adcirc-mcp",
|
|
4
|
+
"title": "ADCIRC Model Setup MCP Server",
|
|
5
|
+
"description": "ADCIRC model configuration debugging, parameter lookup, and validation",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/mansurjisan/ocean-mcp",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"identifier": "adcirc-mcp",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"runtimeHint": "uvx",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Dual-purpose client: local file reader + ADCIRC wiki documentation fetcher."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
ADCIRC_WIKI_BASE = "https://wiki.adcirc.org"
|
|
8
|
+
WIKI_API_URL = f"{ADCIRC_WIKI_BASE}/w/api.php"
|
|
9
|
+
|
|
10
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ADCIRCClientError(Exception):
|
|
14
|
+
"""Custom exception for ADCIRC client errors."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ADCIRCClient:
|
|
20
|
+
"""Async client for local file reading and ADCIRC wiki access."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._client: httpx.AsyncClient | None = None
|
|
24
|
+
|
|
25
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
26
|
+
if self._client is None or self._client.is_closed:
|
|
27
|
+
self._client = httpx.AsyncClient(timeout=30.0)
|
|
28
|
+
return self._client
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def read_file(file_path: str) -> str:
|
|
32
|
+
"""Read a local file with size limit."""
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
size = os.path.getsize(file_path)
|
|
36
|
+
if size > MAX_FILE_SIZE:
|
|
37
|
+
raise ADCIRCClientError(
|
|
38
|
+
f"File too large ({size / 1024 / 1024:.1f} MB). "
|
|
39
|
+
f"Max is {MAX_FILE_SIZE / 1024 / 1024:.0f} MB."
|
|
40
|
+
)
|
|
41
|
+
with open(file_path) as f:
|
|
42
|
+
return f.read()
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def read_file_header(file_path: str, max_lines: int = 100) -> str:
|
|
46
|
+
"""Read only the first N lines of a file."""
|
|
47
|
+
lines = []
|
|
48
|
+
with open(file_path) as f:
|
|
49
|
+
for i, line in enumerate(f):
|
|
50
|
+
if i >= max_lines:
|
|
51
|
+
break
|
|
52
|
+
lines.append(line)
|
|
53
|
+
return "".join(lines)
|
|
54
|
+
|
|
55
|
+
async def fetch_wiki_page(self, page_title: str) -> str:
|
|
56
|
+
"""Fetch a wiki page and return its content as plain text."""
|
|
57
|
+
client = await self._get_client()
|
|
58
|
+
params = {
|
|
59
|
+
"action": "parse",
|
|
60
|
+
"page": page_title,
|
|
61
|
+
"prop": "text",
|
|
62
|
+
"format": "json",
|
|
63
|
+
}
|
|
64
|
+
response = await client.get(WIKI_API_URL, params=params)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
data = response.json()
|
|
67
|
+
|
|
68
|
+
if "error" in data:
|
|
69
|
+
raise ADCIRCClientError(
|
|
70
|
+
f"Wiki page not found: {data['error'].get('info', page_title)}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
html = data.get("parse", {}).get("text", {}).get("*", "")
|
|
74
|
+
return strip_html_to_text(html)
|
|
75
|
+
|
|
76
|
+
async def search_wiki(self, query: str, limit: int = 10) -> list[dict]:
|
|
77
|
+
"""Search the ADCIRC wiki and return matching pages."""
|
|
78
|
+
client = await self._get_client()
|
|
79
|
+
params = {
|
|
80
|
+
"action": "query",
|
|
81
|
+
"list": "search",
|
|
82
|
+
"srsearch": query,
|
|
83
|
+
"srlimit": str(limit),
|
|
84
|
+
"format": "json",
|
|
85
|
+
}
|
|
86
|
+
response = await client.get(WIKI_API_URL, params=params)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
data = response.json()
|
|
89
|
+
|
|
90
|
+
results = []
|
|
91
|
+
for item in data.get("query", {}).get("search", []):
|
|
92
|
+
results.append(
|
|
93
|
+
{
|
|
94
|
+
"title": item.get("title", ""),
|
|
95
|
+
"snippet": strip_html_to_text(item.get("snippet", "")),
|
|
96
|
+
"url": f"{ADCIRC_WIKI_BASE}/wiki/{item.get('title', '').replace(' ', '_')}",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
return results
|
|
100
|
+
|
|
101
|
+
async def close(self) -> None:
|
|
102
|
+
if self._client and not self._client.is_closed:
|
|
103
|
+
await self._client.aclose()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def strip_html_to_text(html: str) -> str:
|
|
107
|
+
"""Strip HTML tags and decode entities to plain text."""
|
|
108
|
+
text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL)
|
|
109
|
+
text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL)
|
|
110
|
+
text = re.sub(r"<[^>]+>", " ", text)
|
|
111
|
+
text = text.replace("&", "&")
|
|
112
|
+
text = text.replace("<", "<")
|
|
113
|
+
text = text.replace(">", ">")
|
|
114
|
+
text = text.replace(""", '"')
|
|
115
|
+
text = text.replace("'", "'")
|
|
116
|
+
text = text.replace(" ", " ")
|
|
117
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
118
|
+
return text
|