ExcelTamer 0.2.0__tar.gz → 0.2.1__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.
Files changed (33) hide show
  1. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/server.py +23 -24
  2. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/PKG-INFO +39 -4
  3. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/SOURCES.txt +1 -0
  4. {exceltamer-0.2.0 → exceltamer-0.2.1}/MANIFEST.in +1 -0
  5. {exceltamer-0.2.0 → exceltamer-0.2.1}/PKG-INFO +39 -4
  6. {exceltamer-0.2.0 → exceltamer-0.2.1}/README.md +38 -3
  7. exceltamer-0.2.1/docs/developer_guide.md +202 -0
  8. {exceltamer-0.2.0 → exceltamer-0.2.1}/pyproject.toml +9 -9
  9. {exceltamer-0.2.0 → exceltamer-0.2.1}/test/test_mcp_smoke.py +91 -0
  10. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/__init__.py +0 -0
  11. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/__init__.py +0 -0
  12. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/audit.py +0 -0
  13. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/config.py +0 -0
  14. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/__init__.py +0 -0
  15. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/diff.py +0 -0
  16. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/read.py +0 -0
  17. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/search.py +0 -0
  18. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/workbook.py +0 -0
  19. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/engine/write.py +0 -0
  20. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/excel.py +0 -0
  21. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/main.py +0 -0
  22. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/prompts/README.md +0 -0
  23. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/prompts/financial_metric_extract.md +0 -0
  24. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/prompts/safe_edit.md +0 -0
  25. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/safety.py +0 -0
  26. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/schemas/__init__.py +0 -0
  27. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer/mcp/sessions.py +0 -0
  28. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/dependency_links.txt +0 -0
  29. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/entry_points.txt +0 -0
  30. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/requires.txt +0 -0
  31. {exceltamer-0.2.0 → exceltamer-0.2.1}/ExcelTamer.egg-info/top_level.txt +0 -0
  32. {exceltamer-0.2.0 → exceltamer-0.2.1}/LICENSE +0 -0
  33. {exceltamer-0.2.0 → exceltamer-0.2.1}/setup.cfg +0 -0
@@ -498,36 +498,35 @@ async def run_stdio():
498
498
  server.create_initialization_options()
499
499
  )
500
500
 
501
- async def run_sse(port: int):
502
- from mcp.server.sse import SseServerTransport
503
- from starlette.applications import Starlette
504
- from starlette.routing import Route
505
- import uvicorn
506
-
507
- sse = SseServerTransport("/messages")
508
-
509
- async def handle_sse(request):
510
- async with sse.connect_sse(
501
+ async def run_sse(port: int):
502
+ from mcp.server.sse import SseServerTransport
503
+ from starlette.applications import Starlette
504
+ from starlette.responses import Response
505
+ from starlette.routing import Mount, Route
506
+ import uvicorn
507
+
508
+ sse = SseServerTransport("/messages/")
509
+
510
+ async def handle_sse(request):
511
+ async with sse.connect_sse(
511
512
  request.scope,
512
513
  request.receive,
513
514
  request._send
514
515
  ) as streams:
515
516
  await server.run(
516
517
  streams[0],
517
- streams[1],
518
- server.create_initialization_options()
519
- )
520
-
521
- async def handle_messages(request):
522
- await sse.handle_post_message(request.scope, request.receive, request._send)
523
-
524
- app = Starlette(
525
- debug=True,
526
- routes=[
527
- Route("/sse", endpoint=handle_sse),
528
- Route("/messages", endpoint=handle_messages, methods=["POST"])
529
- ]
530
- )
518
+ streams[1],
519
+ server.create_initialization_options()
520
+ )
521
+ return Response()
522
+
523
+ app = Starlette(
524
+ debug=True,
525
+ routes=[
526
+ Route("/sse", endpoint=handle_sse, methods=["GET"]),
527
+ Mount("/messages/", app=sse.handle_post_message),
528
+ ]
529
+ )
531
530
 
532
531
  config = uvicorn.Config(app, host="0.0.0.0", port=port, log_level="info")
533
532
  server_instance = uvicorn.Server(config)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExcelTamer
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Model Context Protocol server for safe Microsoft Excel automation
5
5
  Author: Shamit
6
6
  License-Expression: GPL-3.0-only
@@ -33,6 +33,9 @@ client.
33
33
  below or read the
34
34
  [complete MCP usage guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/mcp_server.md).
35
35
 
36
+ **Building or contributing?** Read the
37
+ [developer guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/developer_guide.md).
38
+
36
39
  ## Quick start: use ExcelTamer with an MCP client
37
40
 
38
41
  ### 1. Install ExcelTamer
@@ -41,13 +44,19 @@ ExcelTamer requires Windows with Microsoft Excel installed and Python 3.11 or
41
44
  newer.
42
45
 
43
46
  ```powershell
44
- pip install .
47
+ pip install ExcelTamer
45
48
  ```
46
49
 
47
50
  ### 2. Add ExcelTamer to your MCP client
48
51
 
49
- Add this client-neutral server definition to your MCP client's configuration,
50
- replacing the allowed root with the directory containing your workbooks:
52
+ Choose either stdio or SSE, depending on the transports supported by your MCP
53
+ client.
54
+
55
+ #### Stdio
56
+
57
+ For the default stdio transport, add this server definition to your MCP
58
+ client's configuration, replacing the allowed root with the directory
59
+ containing your workbooks:
51
60
 
52
61
  ```json
53
62
  {
@@ -66,6 +75,32 @@ replacing the allowed root with the directory containing your workbooks:
66
75
  access. The server defaults to read-only workbook mode and records write
67
76
  operations in an audit log.
68
77
 
78
+ #### SSE
79
+
80
+ To use SSE, start ExcelTamer separately in PowerShell. Set server environment
81
+ variables in the same shell:
82
+
83
+ ```powershell
84
+ $env:EXCELTAMER_MCP_ALLOWED_ROOTS = "C:\Users\you\Documents\Excel"
85
+ exceltamer-mcp --port 8123
86
+ ```
87
+
88
+ Then configure an SSE-capable MCP client to connect to the stream endpoint:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "exceltamer": {
94
+ "url": "http://127.0.0.1:8123/sse"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Client configuration field names can vary; use
101
+ `http://127.0.0.1:8123/sse` as the server URL. ExcelTamer receives client
102
+ messages at `http://127.0.0.1:8123/messages/`.
103
+
69
104
  ### 3. Restart the client and ask it to use Excel
70
105
 
71
106
  For inspection, try:
@@ -27,4 +27,5 @@ ExcelTamer/mcp/prompts/README.md
27
27
  ExcelTamer/mcp/prompts/financial_metric_extract.md
28
28
  ExcelTamer/mcp/prompts/safe_edit.md
29
29
  ExcelTamer/mcp/schemas/__init__.py
30
+ docs/developer_guide.md
30
31
  test/test_mcp_smoke.py
@@ -1,3 +1,4 @@
1
1
  include LICENSE
2
2
  include README.md
3
+ include docs/developer_guide.md
3
4
  recursive-include ExcelTamer/mcp/prompts *.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExcelTamer
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Model Context Protocol server for safe Microsoft Excel automation
5
5
  Author: Shamit
6
6
  License-Expression: GPL-3.0-only
@@ -33,6 +33,9 @@ client.
33
33
  below or read the
34
34
  [complete MCP usage guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/mcp_server.md).
35
35
 
36
+ **Building or contributing?** Read the
37
+ [developer guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/developer_guide.md).
38
+
36
39
  ## Quick start: use ExcelTamer with an MCP client
37
40
 
38
41
  ### 1. Install ExcelTamer
@@ -41,13 +44,19 @@ ExcelTamer requires Windows with Microsoft Excel installed and Python 3.11 or
41
44
  newer.
42
45
 
43
46
  ```powershell
44
- pip install .
47
+ pip install ExcelTamer
45
48
  ```
46
49
 
47
50
  ### 2. Add ExcelTamer to your MCP client
48
51
 
49
- Add this client-neutral server definition to your MCP client's configuration,
50
- replacing the allowed root with the directory containing your workbooks:
52
+ Choose either stdio or SSE, depending on the transports supported by your MCP
53
+ client.
54
+
55
+ #### Stdio
56
+
57
+ For the default stdio transport, add this server definition to your MCP
58
+ client's configuration, replacing the allowed root with the directory
59
+ containing your workbooks:
51
60
 
52
61
  ```json
53
62
  {
@@ -66,6 +75,32 @@ replacing the allowed root with the directory containing your workbooks:
66
75
  access. The server defaults to read-only workbook mode and records write
67
76
  operations in an audit log.
68
77
 
78
+ #### SSE
79
+
80
+ To use SSE, start ExcelTamer separately in PowerShell. Set server environment
81
+ variables in the same shell:
82
+
83
+ ```powershell
84
+ $env:EXCELTAMER_MCP_ALLOWED_ROOTS = "C:\Users\you\Documents\Excel"
85
+ exceltamer-mcp --port 8123
86
+ ```
87
+
88
+ Then configure an SSE-capable MCP client to connect to the stream endpoint:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "exceltamer": {
94
+ "url": "http://127.0.0.1:8123/sse"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Client configuration field names can vary; use
101
+ `http://127.0.0.1:8123/sse` as the server URL. ExcelTamer receives client
102
+ messages at `http://127.0.0.1:8123/messages/`.
103
+
69
104
  ### 3. Restart the client and ask it to use Excel
70
105
 
71
106
  For inspection, try:
@@ -9,6 +9,9 @@ client.
9
9
  below or read the
10
10
  [complete MCP usage guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/mcp_server.md).
11
11
 
12
+ **Building or contributing?** Read the
13
+ [developer guide](https://github.com/shamitv/ExcelTamer/blob/main/docs/developer_guide.md).
14
+
12
15
  ## Quick start: use ExcelTamer with an MCP client
13
16
 
14
17
  ### 1. Install ExcelTamer
@@ -17,13 +20,19 @@ ExcelTamer requires Windows with Microsoft Excel installed and Python 3.11 or
17
20
  newer.
18
21
 
19
22
  ```powershell
20
- pip install .
23
+ pip install ExcelTamer
21
24
  ```
22
25
 
23
26
  ### 2. Add ExcelTamer to your MCP client
24
27
 
25
- Add this client-neutral server definition to your MCP client's configuration,
26
- replacing the allowed root with the directory containing your workbooks:
28
+ Choose either stdio or SSE, depending on the transports supported by your MCP
29
+ client.
30
+
31
+ #### Stdio
32
+
33
+ For the default stdio transport, add this server definition to your MCP
34
+ client's configuration, replacing the allowed root with the directory
35
+ containing your workbooks:
27
36
 
28
37
  ```json
29
38
  {
@@ -42,6 +51,32 @@ replacing the allowed root with the directory containing your workbooks:
42
51
  access. The server defaults to read-only workbook mode and records write
43
52
  operations in an audit log.
44
53
 
54
+ #### SSE
55
+
56
+ To use SSE, start ExcelTamer separately in PowerShell. Set server environment
57
+ variables in the same shell:
58
+
59
+ ```powershell
60
+ $env:EXCELTAMER_MCP_ALLOWED_ROOTS = "C:\Users\you\Documents\Excel"
61
+ exceltamer-mcp --port 8123
62
+ ```
63
+
64
+ Then configure an SSE-capable MCP client to connect to the stream endpoint:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "exceltamer": {
70
+ "url": "http://127.0.0.1:8123/sse"
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ Client configuration field names can vary; use
77
+ `http://127.0.0.1:8123/sse` as the server URL. ExcelTamer receives client
78
+ messages at `http://127.0.0.1:8123/messages/`.
79
+
45
80
  ### 3. Restart the client and ask it to use Excel
46
81
 
47
82
  For inspection, try:
@@ -0,0 +1,202 @@
1
+ # ExcelTamer Developer Guide
2
+
3
+ This guide covers the Windows development workflow for ExcelTamer: setting up
4
+ an editable checkout, navigating the MCP implementation, running automated and
5
+ live validation, and building release artifacts. For MCP client configuration
6
+ and the public protocol reference, see the [MCP server guide](mcp_server.md).
7
+
8
+ ## Prerequisites
9
+
10
+ - Windows
11
+ - Python 3.11, 3.12, 3.13, or 3.14
12
+ - Git
13
+ - Microsoft Excel for live workbook validation
14
+
15
+ Microsoft Excel is not required to run the automated smoke tests or build the
16
+ package.
17
+
18
+ ## Set up a development environment
19
+
20
+ Clone the repository and enter its directory:
21
+
22
+ ```powershell
23
+ git clone https://github.com/shamitv/ExcelTamer.git
24
+ Set-Location ExcelTamer
25
+ ```
26
+
27
+ Create and activate a virtual environment:
28
+
29
+ ```powershell
30
+ py -m venv .venv
31
+ .\.venv\Scripts\Activate.ps1
32
+ python --version
33
+ ```
34
+
35
+ The reported Python version must be between 3.11 and 3.14. Install ExcelTamer
36
+ in editable mode, then install the build-validation tools:
37
+
38
+ ```powershell
39
+ python -m pip install --upgrade pip
40
+ python -m pip install -e .
41
+ python -m pip install build twine
42
+ ```
43
+
44
+ `build` and `twine` are development tools and are intentionally not installed
45
+ as ExcelTamer runtime dependencies.
46
+
47
+ ## Repository layout
48
+
49
+ | Path | Purpose |
50
+ | --- | --- |
51
+ | `pyproject.toml` | Package metadata, supported Python versions, runtime dependencies, and the `exceltamer-mcp` entry point |
52
+ | `ExcelTamer/mcp/main.py` | CLI parsing and stdio/SSE transport selection |
53
+ | `ExcelTamer/mcp/server.py` | MCP tools, resources, prompts, request dispatch, and transport hosts |
54
+ | `ExcelTamer/mcp/engine/` | Workbook lifecycle, read, write, search, checkpoint, and audit-history operations |
55
+ | `ExcelTamer/mcp/excel.py` | Internal xlwings backend |
56
+ | `ExcelTamer/mcp/config.py` | Environment-driven server settings |
57
+ | `ExcelTamer/mcp/safety.py` | Workbook path validation |
58
+ | `ExcelTamer/mcp/audit.py` and `sessions.py` | Write-audit persistence and active workbook sessions |
59
+ | `ExcelTamer/mcp/prompts/` | Markdown bodies for the packaged MCP prompts |
60
+ | `test/test_mcp_smoke.py` | Automated protocol and engine smoke tests |
61
+ | `test/mcp_client.py` | Standalone stdio/SSE validation client |
62
+ | `docs/excel/` | Internal Excel behavior, constraints, and threading documentation |
63
+
64
+ The MCP protocol is the public integration boundary. Treat the xlwings backend
65
+ and engine modules as internal implementation details.
66
+
67
+ ## Run the development server
68
+
69
+ After the editable install, the `exceltamer-mcp` command runs the current
70
+ checkout.
71
+
72
+ ### Stdio
73
+
74
+ Run the default stdio transport:
75
+
76
+ ```powershell
77
+ exceltamer-mcp
78
+ ```
79
+
80
+ The equivalent module command is:
81
+
82
+ ```powershell
83
+ python -m ExcelTamer.mcp.main
84
+ ```
85
+
86
+ The standalone client starts its own stdio server subprocess, performs MCP
87
+ discovery, retrieves the packaged prompts, and exits:
88
+
89
+ ```powershell
90
+ python test/mcp_client.py
91
+ ```
92
+
93
+ ### SSE
94
+
95
+ Start the SSE server in one PowerShell window. Environment variables must be
96
+ set in the server process:
97
+
98
+ ```powershell
99
+ $env:EXCELTAMER_MCP_ALLOWED_ROOTS = (Get-Location).Path
100
+ exceltamer-mcp --port 8123
101
+ ```
102
+
103
+ In a second activated PowerShell window, connect the validation client:
104
+
105
+ ```powershell
106
+ python test/mcp_client.py --transport sse --port 8123
107
+ ```
108
+
109
+ The SSE stream is available at `http://127.0.0.1:8123/sse`; client messages are
110
+ posted to `http://127.0.0.1:8123/messages/`. See the
111
+ [configuration reference](mcp_server.md#configuration) for every environment
112
+ variable that influences the server.
113
+
114
+ ## Test changes
115
+
116
+ Run the automated suite from the repository root:
117
+
118
+ ```powershell
119
+ python -m unittest discover -s test -p "test_*.py" -v
120
+ ```
121
+
122
+ The current suite runs eight tests covering the MCP surface, packaged prompts,
123
+ range normalization, engine read/search/write behavior, and a real stdio
124
+ and SSE handshake. These tests use fakes where workbook behavior is needed and
125
+ do not launch Microsoft Excel.
126
+
127
+ For optional live validation, use the included example workbook:
128
+
129
+ ```powershell
130
+ python test/mcp_client.py --file .\test\example.xlsx
131
+ ```
132
+
133
+ This opens the workbook through Excel in read-only mode, retrieves its
134
+ structure, and closes it. To run the same check against an already running SSE
135
+ server:
136
+
137
+ ```powershell
138
+ python test/mcp_client.py --transport sse --port 8123 --file .\test\example.xlsx
139
+ ```
140
+
141
+ The workbook must be under one of `EXCELTAMER_MCP_ALLOWED_ROOTS`. The default
142
+ allowed root is the current working directory.
143
+
144
+ ## Build and verify packages
145
+
146
+ Run the following workflow from a clean repository checkout with the virtual
147
+ environment activated.
148
+
149
+ ### 1. Confirm the package version
150
+
151
+ The version used in both artifact names comes from `pyproject.toml`:
152
+
153
+ ```powershell
154
+ Select-String -Path .\pyproject.toml -Pattern '^version = '
155
+ git status --short
156
+ ```
157
+
158
+ The current version is `0.2.1`. Review unexpected working-tree changes before
159
+ building.
160
+
161
+ ### 2. Run the automated tests
162
+
163
+ ```powershell
164
+ python -m unittest discover -s test -p "test_*.py" -v
165
+ ```
166
+
167
+ ### 3. Build the source and binary distributions
168
+
169
+ ```powershell
170
+ python -m build
171
+ ```
172
+
173
+ For version `0.2.1`, this creates:
174
+
175
+ - `dist\exceltamer-0.2.1.tar.gz` — source distribution
176
+ - `dist\exceltamer-0.2.1-py3-none-any.whl` — binary wheel
177
+
178
+ The `dist/` directory is ignored by Git. If it contains artifacts from other
179
+ versions, verify only the files for the version being prepared.
180
+
181
+ ### 4. Check PyPI metadata and README rendering
182
+
183
+ ```powershell
184
+ python -m twine check `
185
+ .\dist\exceltamer-0.2.1.tar.gz `
186
+ .\dist\exceltamer-0.2.1-py3-none-any.whl
187
+ ```
188
+
189
+ Both artifacts must report `PASSED`.
190
+
191
+ ### 5. Generate optional SHA-256 hashes
192
+
193
+ ```powershell
194
+ Get-FileHash -Algorithm SHA256 `
195
+ .\dist\exceltamer-0.2.1.tar.gz, `
196
+ .\dist\exceltamer-0.2.1-py3-none-any.whl
197
+ ```
198
+
199
+ Record these hashes when artifacts are transferred or retained for a release.
200
+
201
+ Uploading to TestPyPI or PyPI, managing credentials, and publishing a release
202
+ are intentionally outside the scope of this guide.
@@ -1,19 +1,19 @@
1
- [build-system]
2
- requires = ["setuptools>=83.0.0"]
3
- build-backend = "setuptools.build_meta"
4
-
1
+ [build-system]
2
+ requires = ["setuptools>=83.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
5
  [project]
6
6
  name = "ExcelTamer"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "A Model Context Protocol server for safe Microsoft Excel automation"
9
9
  readme = "README.md"
10
10
  authors = [
11
11
  { name = "Shamit" },
12
12
  ]
13
- license = "GPL-3.0-only"
14
- license-files = ["LICENSE"]
15
- classifiers = [
16
- "Programming Language :: Python :: 3",
13
+ license = "GPL-3.0-only"
14
+ license-files = ["LICENSE"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
17
  "Programming Language :: Python :: 3.11",
18
18
  "Programming Language :: Python :: 3.12",
19
19
  "Programming Language :: Python :: 3.13",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import os
5
+ import socket
5
6
  import sys
6
7
  import unittest
7
8
  from types import SimpleNamespace
@@ -9,6 +10,7 @@ from unittest.mock import patch
9
10
 
10
11
  import pandas as pd
11
12
  from mcp import ClientSession, StdioServerParameters
13
+ from mcp.client.sse import sse_client
12
14
  from mcp.client.stdio import stdio_client
13
15
 
14
16
  from ExcelTamer.mcp.engine import read, search, write
@@ -191,5 +193,94 @@ class StdioHandshakeTests(unittest.IsolatedAsyncioTestCase):
191
193
  self.assertTrue(safe_edit.messages[0].content.text.strip())
192
194
 
193
195
 
196
+ class SseHandshakeTests(unittest.IsolatedAsyncioTestCase):
197
+ async def test_sse_discovery_and_prompt_retrieval(self):
198
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as port_socket:
199
+ port_socket.bind(("127.0.0.1", 0))
200
+ port = port_socket.getsockname()[1]
201
+
202
+ env = os.environ.copy()
203
+ env["PYTHONPATH"] = os.pathsep.join(
204
+ path for path in (os.getcwd(), env.get("PYTHONPATH")) if path
205
+ )
206
+ process = await asyncio.create_subprocess_exec(
207
+ sys.executable,
208
+ "-m",
209
+ "ExcelTamer.mcp.main",
210
+ "--port",
211
+ str(port),
212
+ env=env,
213
+ stdout=asyncio.subprocess.DEVNULL,
214
+ stderr=asyncio.subprocess.PIPE,
215
+ )
216
+ stderr_text = ""
217
+ try:
218
+ await self._wait_for_listener(process, port)
219
+ async with asyncio.timeout(10):
220
+ async with sse_client(
221
+ f"http://127.0.0.1:{port}/sse",
222
+ timeout=5,
223
+ sse_read_timeout=5,
224
+ ) as streams:
225
+ async with ClientSession(*streams) as session:
226
+ await session.initialize()
227
+ tools = await session.list_tools()
228
+ resources = await session.list_resources()
229
+ prompts = await session.list_prompts()
230
+ safe_edit = await session.get_prompt("safe-edit")
231
+
232
+ self.assertEqual(len(tools.tools), 15)
233
+ self.assertEqual(len(resources.resources), 1)
234
+ self.assertEqual(len(prompts.prompts), 2)
235
+ self.assertTrue(safe_edit.messages[0].content.text.strip())
236
+
237
+ # Give the request handler time to finish after the SSE client closes.
238
+ await asyncio.sleep(0.1)
239
+ finally:
240
+ if process.returncode is None:
241
+ process.terminate()
242
+ try:
243
+ await asyncio.wait_for(process.wait(), timeout=5)
244
+ except TimeoutError:
245
+ process.kill()
246
+ await process.wait()
247
+ if process.stderr is not None:
248
+ stderr_text = (await process.stderr.read()).decode(
249
+ errors="replace"
250
+ )
251
+
252
+ self.assertNotIn("Exception in ASGI application", stderr_text)
253
+ self.assertNotIn(
254
+ "TypeError: 'NoneType' object is not callable",
255
+ stderr_text,
256
+ )
257
+
258
+ async def _wait_for_listener(self, process, port):
259
+ loop = asyncio.get_running_loop()
260
+ deadline = loop.time() + 10
261
+ while loop.time() < deadline:
262
+ if process.returncode is not None:
263
+ stderr = ""
264
+ if process.stderr is not None:
265
+ stderr = (await process.stderr.read()).decode(
266
+ errors="replace"
267
+ )
268
+ self.fail(
269
+ f"SSE server exited before accepting connections:\n{stderr}"
270
+ )
271
+ try:
272
+ _reader, writer = await asyncio.open_connection(
273
+ "127.0.0.1",
274
+ port,
275
+ )
276
+ except OSError:
277
+ await asyncio.sleep(0.05)
278
+ continue
279
+ writer.close()
280
+ await writer.wait_closed()
281
+ return
282
+ self.fail(f"SSE server did not listen on port {port} within 10 seconds")
283
+
284
+
194
285
  if __name__ == "__main__":
195
286
  unittest.main()
File without changes
File without changes