tempura-client 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.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: tempura-client
3
+ Version: 0.1.0
4
+ Summary: MCP client that vends short-term AWS credentials via stdio transport
5
+ License: MIT
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mcp[cli]
9
+ Requires-Dist: httpx
10
+
11
+ # tempura-client
12
+
13
+ A lightweight MCP (Model Context Protocol) client that vends short-term AWS
14
+ credentials via stdio transport.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install tempura-client
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The package provides a `tempura-client` command that communicates over
25
+ stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
26
+ by an MCP-aware agent or host application.
27
+
28
+ ```bash
29
+ tempura-client
30
+ ```
31
+
32
+ ## Dependencies
33
+
34
+ - `mcp[cli]` - Model Context Protocol SDK
35
+ - `httpx` - HTTP client for forwarding requests to the remote server
@@ -0,0 +1,25 @@
1
+ # tempura-client
2
+
3
+ A lightweight MCP (Model Context Protocol) client that vends short-term AWS
4
+ credentials via stdio transport.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install tempura-client
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ The package provides a `tempura-client` command that communicates over
15
+ stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
16
+ by an MCP-aware agent or host application.
17
+
18
+ ```bash
19
+ tempura-client
20
+ ```
21
+
22
+ ## Dependencies
23
+
24
+ - `mcp[cli]` - Model Context Protocol SDK
25
+ - `httpx` - HTTP client for forwarding requests to the remote server
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tempura-client"
7
+ version = "0.1.0"
8
+ description = "MCP client that vends short-term AWS credentials via stdio transport"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = {text = "MIT"}
12
+ dependencies = [
13
+ "mcp[cli]",
14
+ "httpx",
15
+ ]
16
+
17
+ [project.scripts]
18
+ tempura-client = "tempura_client.app:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+ include = ["tempura_client*", "tempura_shared*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Tempura MCP Client.
2
+
3
+ A local MCP proxy that forwards get_credentials requests to
4
+ the deployed Tempura Server over HTTPS and persists temporary
5
+ credentials to the host AWS credentials file.
6
+ """
@@ -0,0 +1,8 @@
1
+ """Entry point for running the MCP Client proxy.
2
+
3
+ Usage: python -m tempura_client
4
+ """
5
+
6
+ from tempura_client.app import main
7
+
8
+ main()
@@ -0,0 +1,12 @@
1
+ """Application entry point for the Tempura MCP Client.
2
+
3
+ Provides main() as the console-script target so that the client
4
+ can be launched via ``tempura-client`` or ``python -m tempura_client``.
5
+ """
6
+
7
+
8
+ def main() -> None:
9
+ """Start the Tempura MCP Client stdio server."""
10
+ from tempura_client.server import mcp
11
+
12
+ mcp.run()
@@ -0,0 +1,61 @@
1
+ """Client configuration loader.
2
+
3
+ Reads required environment variables and returns a typed config object.
4
+ Raises ConfigError if any required variable is missing or empty.
5
+
6
+ Requirements: 4.1, 4.2, 4.4, 4.5
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass
13
+
14
+
15
+ class ConfigError(Exception):
16
+ """Raised when required client configuration is missing or invalid."""
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ClientConfig:
21
+ """Immutable container for client configuration.
22
+
23
+ Fields:
24
+ api_key: The API Gateway API key value (from TEMPURA_API_KEY).
25
+ endpoint: The Server endpoint URL (from TEMPURA_ENDPOINT).
26
+ """
27
+
28
+ api_key: str
29
+ endpoint: str
30
+
31
+
32
+ def load_config() -> ClientConfig:
33
+ """Load client configuration from environment variables.
34
+
35
+ Reads TEMPURA_API_KEY and TEMPURA_ENDPOINT from the process
36
+ environment. Raises ConfigError with a clear message identifying
37
+ the missing variable if either is unset or empty.
38
+
39
+ Returns:
40
+ ClientConfig with the validated values.
41
+
42
+ Raises:
43
+ ConfigError: If TEMPURA_API_KEY or TEMPURA_ENDPOINT is
44
+ missing or empty.
45
+ """
46
+ api_key = os.environ.get("TEMPURA_API_KEY", "")
47
+ if not api_key:
48
+ raise ConfigError(
49
+ "TEMPURA_API_KEY environment variable is required but "
50
+ "not set or empty. Set it to your API Gateway API key."
51
+ )
52
+
53
+ endpoint = os.environ.get("TEMPURA_ENDPOINT", "")
54
+ if not endpoint:
55
+ raise ConfigError(
56
+ "TEMPURA_ENDPOINT environment variable is required but "
57
+ "not set or empty. Set it to the Tempura Server URL "
58
+ "(e.g. https://<api-id>.execute-api.<region>.amazonaws.com/prod/mcp)."
59
+ )
60
+
61
+ return ClientConfig(api_key=api_key, endpoint=endpoint)
@@ -0,0 +1,120 @@
1
+ """Remote HTTP forwarder for the Tempura Client.
2
+
3
+ POSTs a plain JSON request to the deployed Tempura Server REST API
4
+ with the x-api-key header, parses the JSON response, and returns
5
+ credential fields. Never logs or surfaces the API key.
6
+
7
+ Requirements: 3.1, 3.2, 3.3, 3.4, 4.3, 4.6, 6.1, 6.2, 6.3, 6.4, 6.6
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ import httpx
15
+
16
+
17
+ class RemoteError(Exception):
18
+ """Raised when the Server returns an error or is unreachable."""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class VendResult:
23
+ """Immutable container for credentials returned by the Server.
24
+
25
+ Fields:
26
+ access_key_id: AWS access key ID.
27
+ secret_access_key: AWS secret access key.
28
+ session_token: AWS session token.
29
+ expiration: ISO-8601 expiration timestamp.
30
+ """
31
+
32
+ access_key_id: str
33
+ secret_access_key: str
34
+ session_token: str
35
+ expiration: str
36
+
37
+
38
+ def vend(account_id: str, api_key: str, endpoint: str) -> VendResult:
39
+ """Forward a get_credentials request to the Tempura Server.
40
+
41
+ Sends a plain JSON POST to the Server endpoint with x-api-key
42
+ authentication. Maps HTTP status codes and Server error responses
43
+ to RemoteError. Never includes the API key in any exception.
44
+
45
+ Args:
46
+ account_id: Validated 12-digit AWS account ID.
47
+ api_key: API Gateway key value (never logged/returned).
48
+ endpoint: Server HTTPS URL.
49
+
50
+ Returns:
51
+ VendResult with temporary credential fields.
52
+
53
+ Raises:
54
+ RemoteError: On authentication failure, rate limiting,
55
+ connectivity issues, or Server-reported errors.
56
+ """
57
+ headers = {
58
+ "x-api-key": api_key,
59
+ "Content-Type": "application/json",
60
+ }
61
+ body = {"account_id": account_id}
62
+
63
+ try:
64
+ response = httpx.post(
65
+ endpoint,
66
+ headers=headers,
67
+ json=body,
68
+ timeout=30.0,
69
+ )
70
+ except httpx.TimeoutException:
71
+ raise RemoteError(
72
+ "Connection timed out reaching the Tempura Server. "
73
+ "Check that TEMPURA_ENDPOINT is correct and the "
74
+ "Server is reachable."
75
+ )
76
+ except httpx.ConnectError:
77
+ raise RemoteError(
78
+ "Unable to connect to the Tempura Server. "
79
+ "Check that TEMPURA_ENDPOINT is correct and the "
80
+ "Server is reachable."
81
+ )
82
+ except httpx.HTTPError:
83
+ raise RemoteError(
84
+ "Connectivity error reaching the Tempura Server. "
85
+ "Check that TEMPURA_ENDPOINT is correct and the "
86
+ "Server is reachable."
87
+ )
88
+
89
+ if response.status_code == 400:
90
+ data = response.json()
91
+ raise RemoteError(data.get("error", "Bad request"))
92
+
93
+ if response.status_code == 403:
94
+ raise RemoteError(
95
+ "Authentication failed. Verify that your "
96
+ "TEMPURA_API_KEY is valid and active."
97
+ )
98
+
99
+ if response.status_code == 429:
100
+ raise RemoteError(
101
+ "Rate limited by the Tempura Server. " "Please wait and try again."
102
+ )
103
+
104
+ if response.status_code == 500:
105
+ data = response.json()
106
+ raise RemoteError(data.get("error", "Server error"))
107
+
108
+ if response.status_code != 200:
109
+ raise RemoteError(
110
+ f"Unexpected response from the Tempura Server "
111
+ f"(HTTP {response.status_code})."
112
+ )
113
+
114
+ creds = response.json()
115
+ return VendResult(
116
+ access_key_id=creds["access_key_id"],
117
+ secret_access_key=creds["secret_access_key"],
118
+ session_token=creds["session_token"],
119
+ expiration=creds["expiration"],
120
+ )
@@ -0,0 +1,81 @@
1
+ """MCP Client proxy server.
2
+
3
+ Registers the get_credentials proxy tool over stdio transport.
4
+ Validates account_id locally, loads config, forwards to the remote
5
+ Server, and persists credentials to the host credentials file.
6
+
7
+ Requirements: 2.1, 2.2, 2.3, 2.5, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 6.5
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+
14
+ from mcp.server.fastmcp import FastMCP
15
+
16
+ from tempura_shared.credentials_file import write_profile
17
+ from tempura_shared.validators import validate_account_id
18
+ from tempura_client.config import ConfigError, load_config
19
+ from tempura_client.remote import RemoteError, vend
20
+
21
+ mcp = FastMCP("tempura-client")
22
+
23
+ _TARGET_PROFILE: str = "TargetReadOnly"
24
+ _CREDENTIALS_PATH: str = os.path.join(os.path.expanduser("~"), ".aws", "credentials")
25
+
26
+
27
+ @mcp.tool()
28
+ def get_credentials(account_id: str) -> dict:
29
+ """Get short-term AWS credentials for a target account.
30
+
31
+ Validates the account_id locally, forwards the request to the
32
+ deployed Tempura Server over HTTPS, and writes the returned
33
+ temporary credentials to the [TargetReadOnly] profile in
34
+ ~/.aws/credentials.
35
+
36
+ Args:
37
+ account_id: 12-digit AWS account identifier.
38
+
39
+ Returns:
40
+ Dictionary with status, profile, and expiration on success,
41
+ or {"error": "..."} on failure. Never echoes the secret
42
+ access key.
43
+ """
44
+ # Step 1: Validate account_id before any remote call
45
+ try:
46
+ validated_id = validate_account_id(account_id)
47
+ except ValueError as exc:
48
+ return {"error": str(exc)}
49
+
50
+ # Step 2: Load config; fail fast if missing
51
+ try:
52
+ config = load_config()
53
+ except ConfigError as exc:
54
+ return {"error": str(exc)}
55
+
56
+ # Step 3: Forward to the remote Server
57
+ try:
58
+ creds = vend(validated_id, config.api_key, config.endpoint)
59
+ except RemoteError as exc:
60
+ return {"error": str(exc)}
61
+
62
+ # Step 4: Write credentials to the host file
63
+ try:
64
+ write_profile(
65
+ profile_name=_TARGET_PROFILE,
66
+ access_key_id=creds.access_key_id,
67
+ secret_access_key=creds.secret_access_key,
68
+ session_token=creds.session_token,
69
+ credentials_path=_CREDENTIALS_PATH,
70
+ )
71
+ except PermissionError as exc:
72
+ return {"error": str(exc)}
73
+ except OSError as exc:
74
+ return {"error": f"Failed to write credentials: {exc}"}
75
+
76
+ # Step 5: Return success without echoing the secret
77
+ return {
78
+ "status": "written",
79
+ "profile": _TARGET_PROFILE,
80
+ "expiration": creds.expiration,
81
+ }
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: tempura-client
3
+ Version: 0.1.0
4
+ Summary: MCP client that vends short-term AWS credentials via stdio transport
5
+ License: MIT
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mcp[cli]
9
+ Requires-Dist: httpx
10
+
11
+ # tempura-client
12
+
13
+ A lightweight MCP (Model Context Protocol) client that vends short-term AWS
14
+ credentials via stdio transport.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install tempura-client
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The package provides a `tempura-client` command that communicates over
25
+ stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
26
+ by an MCP-aware agent or host application.
27
+
28
+ ```bash
29
+ tempura-client
30
+ ```
31
+
32
+ ## Dependencies
33
+
34
+ - `mcp[cli]` - Model Context Protocol SDK
35
+ - `httpx` - HTTP client for forwarding requests to the remote server
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/tempura_client/__init__.py
4
+ src/tempura_client/__main__.py
5
+ src/tempura_client/app.py
6
+ src/tempura_client/config.py
7
+ src/tempura_client/remote.py
8
+ src/tempura_client/server.py
9
+ src/tempura_client.egg-info/PKG-INFO
10
+ src/tempura_client.egg-info/SOURCES.txt
11
+ src/tempura_client.egg-info/dependency_links.txt
12
+ src/tempura_client.egg-info/entry_points.txt
13
+ src/tempura_client.egg-info/requires.txt
14
+ src/tempura_client.egg-info/top_level.txt
15
+ src/tempura_shared/__init__.py
16
+ src/tempura_shared/credentials_file.py
17
+ src/tempura_shared/validators.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tempura-client = tempura_client.app:main
@@ -0,0 +1,2 @@
1
+ mcp[cli]
2
+ httpx
@@ -0,0 +1,2 @@
1
+ tempura_client
2
+ tempura_shared
@@ -0,0 +1,5 @@
1
+ """Tempura shared modules.
2
+
3
+ Stdlib-only utilities shared between the tempura server and
4
+ tempura_client packages: credential file I/O and input validators.
5
+ """
@@ -0,0 +1,128 @@
1
+ """Credentials file manager.
2
+
3
+ Read and write AWS credentials file profiles in INI format.
4
+ Requirements: 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 8.3
5
+ """
6
+
7
+ import configparser
8
+ import os
9
+ import stat
10
+
11
+ _DEFAULT_CREDENTIALS_PATH = os.path.join(os.path.expanduser("~"), ".aws", "credentials")
12
+
13
+ _SECURE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0o600
14
+
15
+
16
+ def read_profile(
17
+ profile_name: str = "MyAwesomeReadOnly",
18
+ credentials_path: str | None = None,
19
+ ) -> dict[str, str]:
20
+ """Read credentials for a named profile from the AWS credentials file.
21
+
22
+ Args:
23
+ profile_name: The profile section name to read.
24
+ credentials_path: Path to the credentials file.
25
+ Defaults to ``~/.aws/credentials``.
26
+
27
+ Returns:
28
+ Dictionary of key-value pairs from the profile section.
29
+
30
+ Raises:
31
+ FileNotFoundError: If the credentials file does not exist.
32
+ KeyError: If the named profile is not found in the file.
33
+ """
34
+ path = credentials_path or _DEFAULT_CREDENTIALS_PATH
35
+
36
+ if not os.path.isfile(path):
37
+ raise FileNotFoundError(
38
+ f"AWS credentials file not found at {path}. "
39
+ "Please configure your credentials by running "
40
+ "'aws configure' or creating the file manually."
41
+ )
42
+
43
+ config = configparser.RawConfigParser()
44
+ config.read(path)
45
+
46
+ if not config.has_section(profile_name):
47
+ raise KeyError(profile_name)
48
+
49
+ return dict(config.items(profile_name))
50
+
51
+
52
+ def _check_permissions(path: str) -> None:
53
+ """Verify the file has secure permissions (mode 600).
54
+
55
+ Raises:
56
+ PermissionError: If the file is group/other readable
57
+ or not writable by the owner.
58
+ """
59
+ file_stat = os.stat(path)
60
+ mode = stat.S_IMODE(file_stat.st_mode)
61
+
62
+ if mode & (stat.S_IRGRP | stat.S_IROTH):
63
+ raise PermissionError(
64
+ f"Credentials file {path} has insecure permissions "
65
+ f"(mode {oct(mode)}). Expected mode 0o600."
66
+ )
67
+
68
+ if not os.access(path, os.W_OK):
69
+ raise PermissionError(f"Credentials file {path} is not writable.")
70
+
71
+
72
+ def write_profile(
73
+ profile_name: str,
74
+ access_key_id: str,
75
+ secret_access_key: str,
76
+ session_token: str | None = None,
77
+ credentials_path: str | None = None,
78
+ ) -> None:
79
+ """Write or update a profile in the AWS credentials file.
80
+
81
+ Args:
82
+ profile_name: The profile section name to write.
83
+ access_key_id: The AWS access key ID.
84
+ secret_access_key: The AWS secret access key.
85
+ session_token: Optional session token.
86
+ credentials_path: Path to the credentials file.
87
+ Defaults to ``~/.aws/credentials``.
88
+
89
+ Raises:
90
+ PermissionError: If the file is not writable or has
91
+ insecure permissions.
92
+ """
93
+ path = credentials_path or _DEFAULT_CREDENTIALS_PATH
94
+ config = configparser.RawConfigParser()
95
+ file_exists = os.path.exists(path)
96
+
97
+ if file_exists:
98
+ _check_permissions(path)
99
+ config.read(path)
100
+ else:
101
+ parent = os.path.dirname(path)
102
+ if parent:
103
+ os.makedirs(parent, exist_ok=True)
104
+
105
+ if not config.has_section(profile_name):
106
+ config.add_section(profile_name)
107
+
108
+ config.set(profile_name, "aws_access_key_id", access_key_id)
109
+ config.set(
110
+ profile_name,
111
+ "aws_secret_access_key",
112
+ secret_access_key,
113
+ )
114
+
115
+ if session_token is not None:
116
+ config.set(
117
+ profile_name,
118
+ "aws_session_token",
119
+ session_token,
120
+ )
121
+ elif config.has_option(profile_name, "aws_session_token"):
122
+ config.remove_option(profile_name, "aws_session_token")
123
+
124
+ with open(path, "w") as f:
125
+ config.write(f)
126
+
127
+ if not file_exists:
128
+ os.chmod(path, _SECURE_MODE)
@@ -0,0 +1,22 @@
1
+ """Input validation functions for tempura.
2
+
3
+ Requirements: 2.1, 2.2, 2.3, 2.4
4
+ """
5
+
6
+ import re
7
+
8
+ _ACCOUNT_ID_PATTERN = re.compile(r"^\d{12}$")
9
+
10
+
11
+ def validate_account_id(account_id: str) -> str:
12
+ """Validate and return a 12-digit AWS account ID string.
13
+
14
+ Raises ValueError if invalid.
15
+ """
16
+ if not isinstance(account_id, str):
17
+ raise ValueError(
18
+ "account_id must be a string, " f"got {type(account_id).__name__}"
19
+ )
20
+ if not _ACCOUNT_ID_PATTERN.match(account_id):
21
+ raise ValueError("account_id must be exactly 12 digits, " f"got '{account_id}'")
22
+ return account_id