Skip to content

Commit ad76450

Browse files
Initial changes to simplechat by Greg 1/22
1 parent cebd8f7 commit ad76450

13 files changed

Lines changed: 484 additions & 15 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ __pycache__
1515

1616
# Azure App Service artifacts
1717
.env
18+
.env.*
19+
**/.env
1820
.pem
1921
.deployment
2022

@@ -30,6 +32,9 @@ priv-*
3032
# temporary files
3133
flask_session
3234

35+
# local user workspace
36+
gunger/
37+
3338
# node modules
3439
/node_modules
3540
/package-lock.json
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Simple Chat - Test Console App
2+
3+
This console app simulates the MCP server login flow:
4+
1) Opens a browser for Azure AD login.
5+
2) Captures the auth code on a local callback.
6+
3) Calls `/getATokenApi?create_session=true` to create a session and receive tokens + `session_id`.
7+
4) Uses the session cookie to create a public workspace and fetch its details.
8+
9+
## Prereqs
10+
- Simple Chat running at `BASE_URL` (default https://localhost:5000)
11+
- Azure AD app registration allows redirect URI:
12+
`http://localhost:8400/callback`
13+
- Public workspaces enabled and user has Create Public Workspace role.
14+
15+
## Setup
16+
1) Create `.env` based on `example.env`.
17+
2) Install dependencies:
18+
- `pip install -r requirements.txt`
19+
20+
## Run
21+
- `python main.py`
22+
23+
## PowerShell Run Script
24+
- `run_test_console.ps1`
25+
26+
## Notes
27+
- If you use a self-signed cert locally, set `VERIFY_SSL=false` in `.env`.
28+
- `SCOPES` must include the same scopes configured in `application/single_app/config.py`.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BASE_URL=https://localhost:5000
2+
TENANT_ID=7d887458-fb0d-40bf-adb3-084d875f65db
3+
CLIENT_ID=0b8c00b9-4dcd-4959-83be-7a0521ce54ce
4+
SCOPES=openid profile offline_access User.Read User.ReadBasic.All People.Read.All Group.Read.All
5+
REDIRECT_HOST=localhost
6+
REDIRECT_PORT=8400
7+
REDIRECT_PATH=/callback
8+
CREATE_SESSION=true
9+
VERIFY_SSL=false
10+
WORKSPACE_NAME=Test Public Workspace
11+
WORKSPACE_DESCRIPTION=Created by test console app
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# main.py
2+
"""Console test client for Simple Chat authentication and API usage."""
3+
4+
import json
5+
import os
6+
import threading
7+
import time
8+
import urllib.parse
9+
import webbrowser
10+
from http.server import BaseHTTPRequestHandler, HTTPServer
11+
from typing import Any, Dict, Optional, Sequence, Tuple, Type, cast
12+
13+
import requests
14+
import dotenv
15+
16+
def load_dotenv(*args: Any, **kwargs: Any) -> bool:
17+
dotenv_module = cast(Any, dotenv)
18+
return bool(dotenv_module.load_dotenv(*args, **kwargs))
19+
20+
21+
def create_auth_handler(auth_state: Dict[str, Any], expected_path: str) -> Type[BaseHTTPRequestHandler]:
22+
class AuthCodeHandler(BaseHTTPRequestHandler):
23+
def do_GET(self) -> None:
24+
parsed = urllib.parse.urlparse(self.path)
25+
if parsed.path != expected_path:
26+
self.send_response(404)
27+
self.end_headers()
28+
self.wfile.write(b"Not Found")
29+
return
30+
31+
query = urllib.parse.parse_qs(parsed.query)
32+
auth_state["code"] = query.get("code", [None])[0]
33+
auth_state["error"] = query.get("error", [None])[0]
34+
auth_state["event"].set()
35+
print("Auth callback received.", flush=True)
36+
37+
self.send_response(200)
38+
self.send_header("Content-Type", "text/html")
39+
self.end_headers()
40+
body = (
41+
"<html><body><h2>Login complete.</h2>"
42+
"<p>You can close this window and return to the console.</p>"
43+
"</body></html>"
44+
)
45+
self.wfile.write(body.encode("utf-8"))
46+
47+
def log_message(self, format: str, *args: object) -> None:
48+
return
49+
50+
return AuthCodeHandler
51+
52+
53+
def build_authorize_url(tenant_id: str, client_id: str, redirect_uri: str, scopes: Sequence[str]) -> str:
54+
base = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize"
55+
params = {
56+
"client_id": client_id,
57+
"response_type": "code",
58+
"redirect_uri": redirect_uri,
59+
"response_mode": "query",
60+
"scope": " ".join(scopes),
61+
"prompt": "select_account"
62+
}
63+
return f"{base}?{urllib.parse.urlencode(params)}"
64+
65+
66+
def wait_for_auth_code(auth_state: Dict[str, Any], timeout_seconds: int = 180) -> Tuple[Optional[str], Optional[str]]:
67+
started = time.time()
68+
last_log = 0.0
69+
while time.time() - started < timeout_seconds:
70+
auth_event = cast(threading.Event, auth_state["event"])
71+
if auth_event.is_set():
72+
code = cast(Optional[str], auth_state.get("code"))
73+
error = cast(Optional[str], auth_state.get("error"))
74+
if not code and error:
75+
return None, error
76+
return code, error
77+
elapsed = time.time() - started
78+
if elapsed - last_log >= 5:
79+
log(f"Waiting for auth code... {int(elapsed)}s")
80+
last_log = elapsed
81+
time.sleep(0.2)
82+
return None, "timeout"
83+
84+
85+
def parse_auth_code_from_input(raw_input: str) -> Optional[str]:
86+
text = raw_input.strip()
87+
if not text:
88+
return None
89+
if text.startswith("http"):
90+
parsed = urllib.parse.urlparse(text)
91+
query = urllib.parse.parse_qs(parsed.query)
92+
return query.get("code", [None])[0]
93+
return text
94+
95+
96+
def log(message: str) -> None:
97+
print(message, flush=True)
98+
99+
100+
def main():
101+
load_dotenv()
102+
103+
base_url = os.getenv("BASE_URL", "https://localhost:5000").rstrip("/")
104+
tenant_id = os.getenv("TENANT_ID")
105+
client_id = os.getenv("CLIENT_ID")
106+
scopes = os.getenv(
107+
"SCOPES",
108+
"openid profile offline_access User.Read User.ReadBasic.All People.Read.All Group.Read.All"
109+
).split()
110+
111+
redirect_host = os.getenv("REDIRECT_HOST", "localhost")
112+
redirect_port = int(os.getenv("REDIRECT_PORT", "8400"))
113+
redirect_path = os.getenv("REDIRECT_PATH", "/callback")
114+
redirect_uri = f"http://{redirect_host}:{redirect_port}{redirect_path}"
115+
116+
create_session = os.getenv("CREATE_SESSION", "true").strip().lower() in ["1", "true", "yes", "y", "on"]
117+
verify_ssl = os.getenv("VERIFY_SSL", "true").strip().lower() in ["1", "true", "yes", "y", "on"]
118+
119+
if not tenant_id or not client_id:
120+
raise SystemExit("TENANT_ID and CLIENT_ID must be set in .env")
121+
122+
auth_state: Dict[str, Any] = {
123+
"event": threading.Event(),
124+
"code": None,
125+
"error": None
126+
}
127+
handler = create_auth_handler(auth_state, redirect_path)
128+
129+
httpd = HTTPServer((redirect_host, redirect_port), handler)
130+
server_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
131+
server_thread.start()
132+
133+
auth_url = build_authorize_url(tenant_id, client_id, redirect_uri, scopes)
134+
log("Opening browser for login...")
135+
webbrowser.open(auth_url)
136+
137+
log("Waiting for auth code...")
138+
auth_code, auth_error = wait_for_auth_code(auth_state, timeout_seconds=240)
139+
log("Auth wait complete. Shutting down local callback server...")
140+
shutdown_thread = threading.Thread(target=httpd.shutdown, daemon=True)
141+
shutdown_thread.start()
142+
shutdown_thread.join(timeout=5)
143+
httpd.server_close()
144+
log("Callback server stopped.")
145+
146+
if auth_error == "timeout" or not auth_code:
147+
log("Auth code not received. Paste the full callback URL or code:")
148+
manual_input = input("callback> ")
149+
auth_code = parse_auth_code_from_input(manual_input)
150+
if not auth_code:
151+
raise SystemExit(f"Authentication failed: {auth_error}")
152+
elif auth_error:
153+
raise SystemExit(f"Authentication failed: {auth_error}")
154+
155+
log("Auth code received. Exchanging for tokens...")
156+
157+
session = requests.Session()
158+
token_url = f"{base_url}/getATokenApi"
159+
token_params = {
160+
"code": auth_code,
161+
"create_session": "true" if create_session else "false",
162+
"redirect_uri": redirect_uri
163+
}
164+
165+
try:
166+
token_response = session.get(token_url, params=token_params, verify=verify_ssl, timeout=30)
167+
log(f"Token response status: {token_response.status_code}")
168+
if token_response.status_code >= 400:
169+
log(f"Token response body: {token_response.text}")
170+
token_response.raise_for_status()
171+
token_payload = token_response.json()
172+
except requests.RequestException as exc:
173+
raise SystemExit(f"Token request failed: {exc}")
174+
175+
session_id = token_payload.get("session_id")
176+
log(f"Session created: {token_payload.get('session_created')} | session_id: {session_id}")
177+
178+
if not create_session:
179+
log("Session not requested. Exiting.")
180+
return
181+
182+
workspace_name = os.getenv("WORKSPACE_NAME", "Test Public Workspace")
183+
workspace_description = os.getenv("WORKSPACE_DESCRIPTION", "Created by test console app")
184+
185+
create_url = f"{base_url}/api/public_workspaces"
186+
create_payload = {
187+
"name": workspace_name,
188+
"description": workspace_description
189+
}
190+
191+
log("Creating public workspace...")
192+
try:
193+
create_response = session.post(create_url, json=create_payload, verify=verify_ssl, timeout=30)
194+
log(f"Create workspace status: {create_response.status_code}")
195+
create_response.raise_for_status()
196+
created = create_response.json()
197+
except requests.RequestException as exc:
198+
raise SystemExit(f"Create workspace failed: {exc}")
199+
200+
workspace_id = created.get("id")
201+
log(f"Created public workspace: {workspace_id} - {created.get('name')}")
202+
203+
if not workspace_id:
204+
raise SystemExit("Workspace creation did not return an id.")
205+
206+
get_url = f"{base_url}/api/public_workspaces/{workspace_id}"
207+
log("Fetching public workspace details...")
208+
try:
209+
get_response = session.get(get_url, verify=verify_ssl, timeout=30)
210+
log(f"Get workspace status: {get_response.status_code}")
211+
get_response.raise_for_status()
212+
except requests.RequestException as exc:
213+
raise SystemExit(f"Get workspace failed: {exc}")
214+
215+
log("Workspace details:")
216+
log(json.dumps(get_response.json(), indent=2))
217+
218+
219+
if __name__ == "__main__":
220+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
python-dotenv==1.0.1
2+
requests==2.32.3
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# run_test_console.ps1
2+
# Run the Simple Chat test console app
3+
4+
$ErrorActionPreference = "Stop"
5+
6+
$appRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
7+
$requirements = Join-Path $appRoot "requirements.txt"
8+
$envFile = Join-Path $appRoot ".env"
9+
10+
if (-not (Test-Path $envFile)) {
11+
Write-Error "Missing .env file at $envFile. Copy example.env to .env and fill values."
12+
exit 1
13+
}
14+
15+
Write-Host "Installing dependencies..."
16+
pip install -r $requirements
17+
18+
Write-Host "Running test console app..."
19+
python (Join-Path $appRoot "main.py")

application/single_app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
EXECUTOR_TYPE = 'thread'
8989
EXECUTOR_MAX_WORKERS = 30
9090
SESSION_TYPE = 'filesystem'
91-
VERSION = "0.235.025"
91+
VERSION = "0.235.042"
9292

9393

9494
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')

0 commit comments

Comments
 (0)