hf-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.
- hf_mcp-0.1.0/MANIFEST.in +9 -0
- hf_mcp-0.1.0/PKG-INFO +84 -0
- hf_mcp-0.1.0/README.md +67 -0
- hf_mcp-0.1.0/config.example.yaml +39 -0
- hf_mcp-0.1.0/docs/configuration.md +90 -0
- hf_mcp-0.1.0/docs/coverage_matrix.md +38 -0
- hf_mcp-0.1.0/docs/export_boundary.md +28 -0
- hf_mcp-0.1.0/docs/security_model.md +25 -0
- hf_mcp-0.1.0/export_manifest.toml +12 -0
- hf_mcp-0.1.0/pyproject.toml +30 -0
- hf_mcp-0.1.0/setup.cfg +4 -0
- hf_mcp-0.1.0/src/hf_mcp/__init__.py +4 -0
- hf_mcp-0.1.0/src/hf_mcp/__main__.py +5 -0
- hf_mcp-0.1.0/src/hf_mcp/annotations.py +18 -0
- hf_mcp-0.1.0/src/hf_mcp/auth.py +178 -0
- hf_mcp-0.1.0/src/hf_mcp/capabilities.py +134 -0
- hf_mcp-0.1.0/src/hf_mcp/cli.py +191 -0
- hf_mcp-0.1.0/src/hf_mcp/config.py +363 -0
- hf_mcp-0.1.0/src/hf_mcp/dispatcher.py +114 -0
- hf_mcp-0.1.0/src/hf_mcp/metadata.py +26 -0
- hf_mcp-0.1.0/src/hf_mcp/normalizers.py +177 -0
- hf_mcp-0.1.0/src/hf_mcp/onboarding.py +219 -0
- hf_mcp-0.1.0/src/hf_mcp/registry.py +334 -0
- hf_mcp-0.1.0/src/hf_mcp/schemas.py +75 -0
- hf_mcp-0.1.0/src/hf_mcp/server.py +105 -0
- hf_mcp-0.1.0/src/hf_mcp/token_store.py +115 -0
- hf_mcp-0.1.0/src/hf_mcp/tools/read_core.py +123 -0
- hf_mcp-0.1.0/src/hf_mcp/tools/read_extended.py +176 -0
- hf_mcp-0.1.0/src/hf_mcp/tools/write_documented.py +134 -0
- hf_mcp-0.1.0/src/hf_mcp/transport.py +87 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/PKG-INFO +84 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/SOURCES.txt +45 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/dependency_links.txt +1 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/entry_points.txt +2 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/requires.txt +2 -0
- hf_mcp-0.1.0/src/hf_mcp.egg-info/top_level.txt +1 -0
- hf_mcp-0.1.0/tests/test_auth_transport.py +368 -0
- hf_mcp-0.1.0/tests/test_capability_policy.py +244 -0
- hf_mcp-0.1.0/tests/test_dispatcher_surface.py +361 -0
- hf_mcp-0.1.0/tests/test_entrypoints.py +175 -0
- hf_mcp-0.1.0/tests/test_export_boundary.py +100 -0
- hf_mcp-0.1.0/tests/test_onboarding_cli.py +133 -0
- hf_mcp-0.1.0/tests/test_read_core.py +187 -0
- hf_mcp-0.1.0/tests/test_read_extended.py +197 -0
- hf_mcp-0.1.0/tests/test_registry_schemas.py +99 -0
- hf_mcp-0.1.0/tests/test_runtime_config.py +207 -0
- hf_mcp-0.1.0/tests/test_write_documented.py +208 -0
hf_mcp-0.1.0/MANIFEST.in
ADDED
hf_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hf-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone Hack Forums MCP launch surface
|
|
5
|
+
Keywords: hackforums,mcp,model-context-protocol
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Topic :: Internet
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
Requires-Dist: mcp>=1.0.0
|
|
17
|
+
|
|
18
|
+
# hf-mcp
|
|
19
|
+
|
|
20
|
+
`hf-mcp` is a standalone MCP server package for the Hack Forums API v2.
|
|
21
|
+
|
|
22
|
+
## Product target
|
|
23
|
+
|
|
24
|
+
The product target is full documented Hack Forums API coverage. The registry and
|
|
25
|
+
coverage matrix include:
|
|
26
|
+
|
|
27
|
+
- core read helpers (`me`, `users`, `forums`, `threads`, `posts`)
|
|
28
|
+
- extended read helpers (`bytes`, `contracts`, `disputes`, `bratings`)
|
|
29
|
+
- later-lane documented read/write rows for `sigmarket`, `contract`, and
|
|
30
|
+
documented `admin/high-risk` helpers
|
|
31
|
+
- documented write helpers (`threads.create`, `posts.reply`, `bytes.transfer`,
|
|
32
|
+
`bytes.deposit`, `bytes.withdraw`, `bytes.bump`)
|
|
33
|
+
|
|
34
|
+
## Exposure model
|
|
35
|
+
|
|
36
|
+
Profiles and overlays are an exposure control layer for a specific user or
|
|
37
|
+
deployment. They do not redefine product scope. If a deployment disables a row,
|
|
38
|
+
that row still remains in the documented product coverage matrix.
|
|
39
|
+
|
|
40
|
+
## Launch surface
|
|
41
|
+
|
|
42
|
+
- console entrypoint: `hf-mcp`
|
|
43
|
+
- module entrypoint: `python -m hf_mcp`
|
|
44
|
+
- `hf-mcp` aliases `hf-mcp serve` (stdio runtime launch)
|
|
45
|
+
- `hf-mcp setup init` creates first-run local config and prints the next commands
|
|
46
|
+
- `hf-mcp doctor` validates local config/secrets/token readiness without network I/O
|
|
47
|
+
- `hf-mcp` ships the `mcp` runtime dependency required for the stdio launch path
|
|
48
|
+
- `hf-mcp serve` runs one stdio server instance and exits when the stdio client disconnects
|
|
49
|
+
- `hf-mcp serve` fails closed on invalid bootstrap state (config/secrets/token/runtime dependency)
|
|
50
|
+
|
|
51
|
+
## First run
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install hf-mcp
|
|
55
|
+
hf-mcp setup init
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Set secrets in your local environment (`.env` or shell):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export HF_MCP_CLIENT_ID=your_client_id
|
|
62
|
+
export HF_MCP_CLIENT_SECRET=your_client_secret
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Finish local bootstrap and readiness checks:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
hf-mcp auth bootstrap
|
|
69
|
+
hf-mcp doctor
|
|
70
|
+
hf-mcp serve
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Source distribution boundary
|
|
74
|
+
|
|
75
|
+
`MANIFEST.in` defines a package-local source distribution boundary rooted in
|
|
76
|
+
`products/hf_mcp`. The source distribution includes package docs, tests, and
|
|
77
|
+
runtime source from this product subtree only.
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
- `docs/configuration.md`
|
|
82
|
+
- `docs/coverage_matrix.md`
|
|
83
|
+
- `docs/security_model.md`
|
|
84
|
+
- `docs/export_boundary.md`
|
hf_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# hf-mcp
|
|
2
|
+
|
|
3
|
+
`hf-mcp` is a standalone MCP server package for the Hack Forums API v2.
|
|
4
|
+
|
|
5
|
+
## Product target
|
|
6
|
+
|
|
7
|
+
The product target is full documented Hack Forums API coverage. The registry and
|
|
8
|
+
coverage matrix include:
|
|
9
|
+
|
|
10
|
+
- core read helpers (`me`, `users`, `forums`, `threads`, `posts`)
|
|
11
|
+
- extended read helpers (`bytes`, `contracts`, `disputes`, `bratings`)
|
|
12
|
+
- later-lane documented read/write rows for `sigmarket`, `contract`, and
|
|
13
|
+
documented `admin/high-risk` helpers
|
|
14
|
+
- documented write helpers (`threads.create`, `posts.reply`, `bytes.transfer`,
|
|
15
|
+
`bytes.deposit`, `bytes.withdraw`, `bytes.bump`)
|
|
16
|
+
|
|
17
|
+
## Exposure model
|
|
18
|
+
|
|
19
|
+
Profiles and overlays are an exposure control layer for a specific user or
|
|
20
|
+
deployment. They do not redefine product scope. If a deployment disables a row,
|
|
21
|
+
that row still remains in the documented product coverage matrix.
|
|
22
|
+
|
|
23
|
+
## Launch surface
|
|
24
|
+
|
|
25
|
+
- console entrypoint: `hf-mcp`
|
|
26
|
+
- module entrypoint: `python -m hf_mcp`
|
|
27
|
+
- `hf-mcp` aliases `hf-mcp serve` (stdio runtime launch)
|
|
28
|
+
- `hf-mcp setup init` creates first-run local config and prints the next commands
|
|
29
|
+
- `hf-mcp doctor` validates local config/secrets/token readiness without network I/O
|
|
30
|
+
- `hf-mcp` ships the `mcp` runtime dependency required for the stdio launch path
|
|
31
|
+
- `hf-mcp serve` runs one stdio server instance and exits when the stdio client disconnects
|
|
32
|
+
- `hf-mcp serve` fails closed on invalid bootstrap state (config/secrets/token/runtime dependency)
|
|
33
|
+
|
|
34
|
+
## First run
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install hf-mcp
|
|
38
|
+
hf-mcp setup init
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Set secrets in your local environment (`.env` or shell):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export HF_MCP_CLIENT_ID=your_client_id
|
|
45
|
+
export HF_MCP_CLIENT_SECRET=your_client_secret
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Finish local bootstrap and readiness checks:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
hf-mcp auth bootstrap
|
|
52
|
+
hf-mcp doctor
|
|
53
|
+
hf-mcp serve
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Source distribution boundary
|
|
57
|
+
|
|
58
|
+
`MANIFEST.in` defines a package-local source distribution boundary rooted in
|
|
59
|
+
`products/hf_mcp`. The source distribution includes package docs, tests, and
|
|
60
|
+
runtime source from this product subtree only.
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
- `docs/configuration.md`
|
|
65
|
+
- `docs/coverage_matrix.md`
|
|
66
|
+
- `docs/security_model.md`
|
|
67
|
+
- `docs/export_boundary.md`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# hf-mcp capability exposure configuration
|
|
2
|
+
#
|
|
3
|
+
# Presets:
|
|
4
|
+
# - reader: read-oriented exposure
|
|
5
|
+
# - forum_operator: read + core content/bytes write exposure
|
|
6
|
+
# - full_api: broadest shipped exposure preset (explicit opt-in)
|
|
7
|
+
# - custom: start empty and explicitly opt in
|
|
8
|
+
#
|
|
9
|
+
# IMPORTANT:
|
|
10
|
+
# Presets are exposure control for a user/deployment. They do not reduce product
|
|
11
|
+
# scope. The product target remains full documented HF API coverage, including
|
|
12
|
+
# later-lane contract, sigmarket, and admin/high-risk rows tracked in
|
|
13
|
+
# docs/coverage_matrix.md.
|
|
14
|
+
|
|
15
|
+
profile: reader
|
|
16
|
+
|
|
17
|
+
# Optional explicit overlays for any profile.
|
|
18
|
+
# These keep naming human-readable and fail closed on unknown values.
|
|
19
|
+
# enabled_capabilities: []
|
|
20
|
+
# enabled_parameter_families: []
|
|
21
|
+
# disabled_capabilities: []
|
|
22
|
+
# disabled_parameter_families: []
|
|
23
|
+
|
|
24
|
+
# Optional runtime path override. Must be absolute and outside this repository.
|
|
25
|
+
# token_path: /home/you/.config/hf_mcp/token.json
|
|
26
|
+
|
|
27
|
+
# Capabilities currently exposed by profile = "full_api":
|
|
28
|
+
# me.read, users.read, forums.read, threads.read, posts.read, bytes.read,
|
|
29
|
+
# contracts.read, disputes.read, bratings.read, sigmarket.market.read,
|
|
30
|
+
# sigmarket.order.read, threads.create, posts.reply, bytes.transfer,
|
|
31
|
+
# bytes.deposit, bytes.withdraw, bytes.bump, contracts.write,
|
|
32
|
+
# sigmarket.write, admin.high_risk.read, admin.high_risk.write
|
|
33
|
+
|
|
34
|
+
# Parameter families currently exposed by profile = "full_api":
|
|
35
|
+
# selectors.user, selectors.forum, selectors.thread, selectors.post,
|
|
36
|
+
# selectors.bytes, selectors.contract, selectors.dispute, selectors.sigmarket,
|
|
37
|
+
# filters.pagination, fields.me.basic, fields.me.advanced,
|
|
38
|
+
# fields.users.profile, fields.posts.body, fields.bytes.amount,
|
|
39
|
+
# writes.content, writes.bytes, confirm.live
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`hf-mcp` uses a presets-first configuration model with fail-closed overlays.
|
|
4
|
+
|
|
5
|
+
## Scope and exposure control
|
|
6
|
+
|
|
7
|
+
The product scope is the full documented Hack Forums API surface captured in the
|
|
8
|
+
coverage matrix. Configuration is an exposure control mechanism for the active
|
|
9
|
+
user or deployment:
|
|
10
|
+
|
|
11
|
+
- `enabled_capabilities` and `disabled_capabilities` decide which tool families
|
|
12
|
+
can register.
|
|
13
|
+
- `enabled_parameter_families` and `disabled_parameter_families` decide which
|
|
14
|
+
schema inputs remain visible and accepted.
|
|
15
|
+
|
|
16
|
+
This means a deployment can disable `sigmarket`, `contract`, or
|
|
17
|
+
`admin/high-risk` rows without redefining the product target.
|
|
18
|
+
|
|
19
|
+
## Profiles
|
|
20
|
+
|
|
21
|
+
- `reader`: read-oriented exposure profile.
|
|
22
|
+
- `forum_operator`: read + core forum/content/bytes write exposure.
|
|
23
|
+
- `full_api`: broadest shipped exposure profile.
|
|
24
|
+
- `custom`: starts empty; caller opts in explicitly.
|
|
25
|
+
|
|
26
|
+
`reader` is the recommended first-run profile. Use `full_api` as an explicit
|
|
27
|
+
opt-in when validating the full documented coverage surface.
|
|
28
|
+
|
|
29
|
+
## First-run setup flow
|
|
30
|
+
|
|
31
|
+
Run the terminal-native onboarding commands in this order:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install hf-mcp
|
|
35
|
+
hf-mcp setup init
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then set local secrets (for example in `.env` or shell):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export HF_MCP_CLIENT_ID=your_client_id
|
|
42
|
+
export HF_MCP_CLIENT_SECRET=your_client_secret
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Complete auth/bootstrap and local readiness validation:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
hf-mcp auth bootstrap
|
|
49
|
+
hf-mcp doctor
|
|
50
|
+
hf-mcp serve
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Runtime configuration responsibilities
|
|
54
|
+
|
|
55
|
+
- YAML is the canonical runtime config input for non-secret policy:
|
|
56
|
+
`profile`, capability overlays, parameter-family overlays, and optional
|
|
57
|
+
`token_path`.
|
|
58
|
+
- `.env` is for secrets and machine-local overrides only (for example
|
|
59
|
+
`HF_MCP_CLIENT_ID`, `HF_MCP_CLIENT_SECRET`, `HF_MCP_TOKEN_PATH`).
|
|
60
|
+
- `.env` must not be used to choose non-secret runtime policy such as
|
|
61
|
+
`profile`, capability families, or parameter families.
|
|
62
|
+
- `token_path` must resolve to an absolute path outside the tracked repository.
|
|
63
|
+
|
|
64
|
+
## Overlays
|
|
65
|
+
|
|
66
|
+
Overlays are additive/subtractive and always validated against known family
|
|
67
|
+
names:
|
|
68
|
+
|
|
69
|
+
- `enabled_capabilities`
|
|
70
|
+
- `disabled_capabilities`
|
|
71
|
+
- `enabled_parameter_families`
|
|
72
|
+
- `disabled_parameter_families`
|
|
73
|
+
|
|
74
|
+
Unknown names fail fast during config load.
|
|
75
|
+
|
|
76
|
+
## Example
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
profile: reader
|
|
80
|
+
|
|
81
|
+
disabled_capabilities:
|
|
82
|
+
- sigmarket.market.read
|
|
83
|
+
- sigmarket.order.read
|
|
84
|
+
|
|
85
|
+
disabled_parameter_families:
|
|
86
|
+
- fields.me.advanced
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The example above is still the same product. It is a narrower exposure profile
|
|
90
|
+
for one deployment.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Coverage Matrix
|
|
2
|
+
|
|
3
|
+
This matrix tracks the public product target for `hf-mcp`: full documented HF
|
|
4
|
+
API coverage. Deployment policy can hide rows, but rows remain in-scope product
|
|
5
|
+
coverage.
|
|
6
|
+
|
|
7
|
+
## Registry-backed rows
|
|
8
|
+
|
|
9
|
+
| Coverage family | Operation | Helper path | Product scope status | Exposure note |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| `me.read` | read | `me` | in scope | can be disabled by capability policy |
|
|
12
|
+
| `users.read` | read | `users` | in scope | can be disabled by capability policy |
|
|
13
|
+
| `forums.read` | read | `forums` | in scope | can be disabled by capability policy |
|
|
14
|
+
| `threads.read` | read | `threads` | in scope | can be disabled by capability policy |
|
|
15
|
+
| `posts.read` | read | `posts` | in scope | can be disabled by capability policy |
|
|
16
|
+
| `bytes.read` | read | `bytes` | in scope | can be disabled by capability policy |
|
|
17
|
+
| `contracts.read` | read | `contracts` | in scope (later-lane row retained) | deployment exposure control may keep disabled |
|
|
18
|
+
| `disputes.read` | read | `disputes` | in scope | can be disabled by capability policy |
|
|
19
|
+
| `bratings.read` | read | `bratings` | in scope | can be disabled by capability policy |
|
|
20
|
+
| `sigmarket.market.read` | read | `sigmarket/market` | in scope (later-lane row retained) | deployment exposure control may keep disabled |
|
|
21
|
+
| `sigmarket.order.read` | read | `sigmarket/order` | in scope (later-lane row retained) | deployment exposure control may keep disabled |
|
|
22
|
+
| `admin.high_risk.read` | read | `admin/high-risk/read` | in scope (documented admin/high-risk row retained) | deployment exposure control may keep disabled |
|
|
23
|
+
| `threads.create` | write | `threads` | in scope | `confirm_live` guard required |
|
|
24
|
+
| `posts.reply` | write | `posts` | in scope | `confirm_live` guard required |
|
|
25
|
+
| `bytes.transfer` | write | `bytes` | in scope | `confirm_live` guard required |
|
|
26
|
+
| `bytes.deposit` | write | `bytes/deposit` | in scope | `confirm_live` guard required |
|
|
27
|
+
| `bytes.withdraw` | write | `bytes/withdraw` | in scope | `confirm_live` guard required |
|
|
28
|
+
| `bytes.bump` | write | `bytes/bump` | in scope | `confirm_live` guard required |
|
|
29
|
+
| `contracts.write` | write | `contracts` | in scope (later-lane contract row retained) | deployment exposure control may keep disabled until helper proof is named |
|
|
30
|
+
| `sigmarket.write` | write | `sigmarket` | in scope (later-lane sigmarket row retained) | deployment exposure control may keep disabled until helper proof is named |
|
|
31
|
+
| `admin.high_risk.write` | write | `admin/high-risk/write` | in scope (documented admin/high-risk row retained) | deployment exposure control may keep disabled until helper proof is named |
|
|
32
|
+
|
|
33
|
+
## Interpretation
|
|
34
|
+
|
|
35
|
+
- A disabled row is still part of product scope.
|
|
36
|
+
- A later-lane row is still part of product scope.
|
|
37
|
+
- If documented HF API coverage expands, this matrix must add named rows rather
|
|
38
|
+
than replacing specifics with vague future language.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Export Boundary
|
|
2
|
+
|
|
3
|
+
`hf-mcp` is published from a product subtree. Export is allowlist-based.
|
|
4
|
+
|
|
5
|
+
## Authoritative manifest
|
|
6
|
+
|
|
7
|
+
`export_manifest.toml` is the machine-readable allowlist for exported files.
|
|
8
|
+
Allowed entries are scoped to `products/hf_mcp/` only.
|
|
9
|
+
|
|
10
|
+
## Included public surfaces
|
|
11
|
+
|
|
12
|
+
- package metadata and launch docs (`pyproject.toml`, `README.md`)
|
|
13
|
+
- example configuration (`config.example.yaml`)
|
|
14
|
+
- public documentation (`docs/**`)
|
|
15
|
+
- package source (`src/hf_mcp/**`)
|
|
16
|
+
- package tests (`tests/**`)
|
|
17
|
+
|
|
18
|
+
## Boundary guarantees
|
|
19
|
+
|
|
20
|
+
- Root-level repository assets are outside the export boundary.
|
|
21
|
+
- Internal orchestration/planning assets are outside the export boundary.
|
|
22
|
+
- Release packaging should fail if any allowlist entry leaves the
|
|
23
|
+
`products/hf_mcp/` subtree.
|
|
24
|
+
- Runtime policy is YAML-first: non-secret policy belongs in `config.yaml`.
|
|
25
|
+
- `.env` is reserved for secrets and machine-local overrides; it must not define
|
|
26
|
+
profile/capability/parameter policy.
|
|
27
|
+
- Token-store location must be an absolute path outside the tracked repository
|
|
28
|
+
tree.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
`hf-mcp` is a networked MCP server for live HF API calls, so every enabled tool
|
|
4
|
+
should be treated as open-world input/output.
|
|
5
|
+
|
|
6
|
+
## Core controls
|
|
7
|
+
|
|
8
|
+
- Capability policy is fail-closed. Unknown capability or parameter-family names
|
|
9
|
+
fail config load.
|
|
10
|
+
- Parameter-family pruning keeps schema exposure aligned with runtime validation.
|
|
11
|
+
- Documented write helpers require explicit `confirm_live = true`.
|
|
12
|
+
- Capability and parameter families can restrict user exposure for high-risk
|
|
13
|
+
rows such as `contract`, `sigmarket`, and `admin/high-risk` helpers.
|
|
14
|
+
|
|
15
|
+
## Secret handling
|
|
16
|
+
|
|
17
|
+
- Keep OAuth client credentials and access tokens in private runtime storage.
|
|
18
|
+
- Do not commit credentials into config files, docs, or tests.
|
|
19
|
+
- Rotate credentials if they are ever exposed.
|
|
20
|
+
|
|
21
|
+
## Operational guidance
|
|
22
|
+
|
|
23
|
+
- Start from read-oriented exposure when validating a new deployment.
|
|
24
|
+
- Enable write families deliberately and verify each path with test coverage.
|
|
25
|
+
- Keep audit logs outside the tracked package tree.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
manifest_version = 1
|
|
2
|
+
package_root = "products/hf_mcp"
|
|
3
|
+
|
|
4
|
+
allowlist = [
|
|
5
|
+
"products/hf_mcp/README.md",
|
|
6
|
+
"products/hf_mcp/pyproject.toml",
|
|
7
|
+
"products/hf_mcp/config.example.yaml",
|
|
8
|
+
"products/hf_mcp/export_manifest.toml",
|
|
9
|
+
"products/hf_mcp/docs/**",
|
|
10
|
+
"products/hf_mcp/src/hf_mcp/**",
|
|
11
|
+
"products/hf_mcp/tests/**",
|
|
12
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hf-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Standalone Hack Forums MCP launch surface"
|
|
9
|
+
keywords = ["hackforums", "mcp", "model-context-protocol"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Topic :: Internet",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
]
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
requires-python = ">=3.11"
|
|
21
|
+
dependencies = ["PyYAML>=6.0", "mcp>=1.0.0"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
hf-mcp = "hf_mcp.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = {"" = "src"}
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
hf_mcp-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .metadata import build_tool_meta
|
|
4
|
+
from .registry import ToolSpec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_annotations(spec: ToolSpec) -> dict[str, object]:
|
|
8
|
+
is_read = spec.operation == "read"
|
|
9
|
+
return {
|
|
10
|
+
"readOnlyHint": is_read,
|
|
11
|
+
"destructiveHint": not is_read,
|
|
12
|
+
"idempotentHint": is_read,
|
|
13
|
+
"openWorldHint": True,
|
|
14
|
+
"_meta": build_tool_meta(spec),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["build_annotations"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
import webbrowser
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
from typing import Mapping
|
|
13
|
+
|
|
14
|
+
from .config import HFMCPSettings
|
|
15
|
+
from .token_store import TokenBundle
|
|
16
|
+
|
|
17
|
+
DEFAULT_AUTHORIZE_ENDPOINT = "https://hackforums.net/api/v2/authorize"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def authorize_via_loopback(settings: HFMCPSettings, open_browser: bool = True) -> TokenBundle:
|
|
21
|
+
client_id = _required_setting(settings, "HF_MCP_CLIENT_ID")
|
|
22
|
+
client_secret = _required_setting(settings, "HF_MCP_CLIENT_SECRET")
|
|
23
|
+
|
|
24
|
+
redirect_uri = settings.runtime_env.get("HF_MCP_REDIRECT_URI", "http://127.0.0.1:8765/callback")
|
|
25
|
+
authorize_url = settings.runtime_env.get("HF_MCP_AUTHORIZE_URL", DEFAULT_AUTHORIZE_ENDPOINT)
|
|
26
|
+
token_url = settings.runtime_env.get("HF_MCP_TOKEN_URL", DEFAULT_AUTHORIZE_ENDPOINT)
|
|
27
|
+
|
|
28
|
+
timeout_raw = settings.runtime_env.get("HF_MCP_AUTH_TIMEOUT_SECONDS", "180")
|
|
29
|
+
try:
|
|
30
|
+
timeout_seconds = int(timeout_raw)
|
|
31
|
+
except (TypeError, ValueError) as exc:
|
|
32
|
+
raise ValueError("HF_MCP_AUTH_TIMEOUT_SECONDS must be an integer number of seconds.") from exc
|
|
33
|
+
|
|
34
|
+
state = secrets.token_urlsafe(24)
|
|
35
|
+
browser_url = _build_authorize_url(
|
|
36
|
+
authorize_url=authorize_url,
|
|
37
|
+
client_id=client_id,
|
|
38
|
+
redirect_uri=redirect_uri,
|
|
39
|
+
state=state,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if open_browser:
|
|
43
|
+
webbrowser.open(browser_url)
|
|
44
|
+
|
|
45
|
+
callback_params = _await_loopback_callback(redirect_uri=redirect_uri, timeout_seconds=timeout_seconds)
|
|
46
|
+
callback_state = callback_params.get("state")
|
|
47
|
+
if callback_state != state:
|
|
48
|
+
raise ValueError("OAuth callback state mismatch. Aborting token exchange.")
|
|
49
|
+
|
|
50
|
+
code = callback_params.get("code")
|
|
51
|
+
if code is None or not code.strip():
|
|
52
|
+
raise ValueError("OAuth callback is missing authorization code.")
|
|
53
|
+
|
|
54
|
+
token_payload = _exchange_code_for_token(
|
|
55
|
+
token_url=token_url,
|
|
56
|
+
client_id=client_id,
|
|
57
|
+
client_secret=client_secret,
|
|
58
|
+
code=code,
|
|
59
|
+
redirect_uri=redirect_uri,
|
|
60
|
+
)
|
|
61
|
+
return TokenBundle.from_payload(token_payload)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_authorize_url(
|
|
65
|
+
authorize_url: str,
|
|
66
|
+
client_id: str,
|
|
67
|
+
redirect_uri: str,
|
|
68
|
+
state: str,
|
|
69
|
+
) -> str:
|
|
70
|
+
params = urllib.parse.urlencode(
|
|
71
|
+
{
|
|
72
|
+
"client_id": client_id,
|
|
73
|
+
"response_type": "code",
|
|
74
|
+
"redirect_uri": redirect_uri,
|
|
75
|
+
"state": state,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
separator = "&" if "?" in authorize_url else "?"
|
|
79
|
+
return f"{authorize_url}{separator}{params}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _await_loopback_callback(redirect_uri: str, timeout_seconds: int) -> Mapping[str, str]:
|
|
83
|
+
parsed_uri = urllib.parse.urlparse(redirect_uri)
|
|
84
|
+
host = parsed_uri.hostname
|
|
85
|
+
port = parsed_uri.port
|
|
86
|
+
callback_path = parsed_uri.path or "/callback"
|
|
87
|
+
|
|
88
|
+
if host not in {"127.0.0.1", "localhost"}:
|
|
89
|
+
raise ValueError("Redirect URI must use localhost or 127.0.0.1 loopback host.")
|
|
90
|
+
if port is None:
|
|
91
|
+
raise ValueError("Redirect URI must include an explicit loopback port.")
|
|
92
|
+
|
|
93
|
+
callback_state: dict[str, str] = {}
|
|
94
|
+
|
|
95
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
96
|
+
def do_GET(self) -> None: # noqa: N802 (standard library method name)
|
|
97
|
+
parsed_request = urllib.parse.urlparse(self.path)
|
|
98
|
+
if parsed_request.path != callback_path:
|
|
99
|
+
self.send_response(404)
|
|
100
|
+
self.end_headers()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
parsed_query = urllib.parse.parse_qs(parsed_request.query, keep_blank_values=True)
|
|
104
|
+
for key in ("code", "state", "error"):
|
|
105
|
+
values = parsed_query.get(key)
|
|
106
|
+
if values:
|
|
107
|
+
callback_state[key] = values[0]
|
|
108
|
+
|
|
109
|
+
self.send_response(200)
|
|
110
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
111
|
+
self.end_headers()
|
|
112
|
+
self.wfile.write(b"You can close this tab and return to the terminal.")
|
|
113
|
+
|
|
114
|
+
def log_message(self, format: str, *args: object) -> None: # noqa: A003
|
|
115
|
+
del format, args
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
server = HTTPServer((host, port), _CallbackHandler)
|
|
119
|
+
server.timeout = 0.25
|
|
120
|
+
|
|
121
|
+
deadline = time.monotonic() + timeout_seconds
|
|
122
|
+
while time.monotonic() < deadline and "code" not in callback_state and "error" not in callback_state:
|
|
123
|
+
server.handle_request()
|
|
124
|
+
|
|
125
|
+
server.server_close()
|
|
126
|
+
|
|
127
|
+
if "error" in callback_state:
|
|
128
|
+
raise ValueError(f"OAuth provider returned error: {callback_state['error']}")
|
|
129
|
+
if "code" not in callback_state:
|
|
130
|
+
raise TimeoutError("Timed out waiting for OAuth loopback callback.")
|
|
131
|
+
return callback_state
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _exchange_code_for_token(
|
|
135
|
+
token_url: str,
|
|
136
|
+
client_id: str,
|
|
137
|
+
client_secret: str,
|
|
138
|
+
code: str,
|
|
139
|
+
redirect_uri: str,
|
|
140
|
+
) -> dict[str, object]:
|
|
141
|
+
encoded_payload = urllib.parse.urlencode(
|
|
142
|
+
{
|
|
143
|
+
"client_id": client_id,
|
|
144
|
+
"client_secret": client_secret,
|
|
145
|
+
"code": code,
|
|
146
|
+
"grant_type": "authorization_code",
|
|
147
|
+
"redirect_uri": redirect_uri,
|
|
148
|
+
}
|
|
149
|
+
).encode("utf-8")
|
|
150
|
+
|
|
151
|
+
request = urllib.request.Request(
|
|
152
|
+
token_url,
|
|
153
|
+
data=encoded_payload,
|
|
154
|
+
method="POST",
|
|
155
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
with urllib.request.urlopen(request, timeout=15) as response:
|
|
160
|
+
response_body = response.read().decode("utf-8")
|
|
161
|
+
except urllib.error.HTTPError as error:
|
|
162
|
+
error_body = error.read().decode("utf-8", errors="replace")
|
|
163
|
+
raise RuntimeError(f"Token exchange failed: HTTP {error.code}. {error_body}") from error
|
|
164
|
+
|
|
165
|
+
parsed = json.loads(response_body)
|
|
166
|
+
if not isinstance(parsed, dict):
|
|
167
|
+
raise ValueError("Token exchange returned non-object JSON payload.")
|
|
168
|
+
return parsed
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _required_setting(settings: HFMCPSettings, name: str) -> str:
|
|
172
|
+
value = settings.runtime_env.get(name) or os.environ.get(name)
|
|
173
|
+
if value is None or not value.strip():
|
|
174
|
+
raise ValueError(f"Missing required environment variable: {name}")
|
|
175
|
+
return value.strip()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["authorize_via_loopback"]
|