dynamic-skill-loader 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.
- dynamic_skill_loader-0.1.0/LICENSE +21 -0
- dynamic_skill_loader-0.1.0/PKG-INFO +165 -0
- dynamic_skill_loader-0.1.0/README.md +150 -0
- dynamic_skill_loader-0.1.0/pyproject.toml +56 -0
- dynamic_skill_loader-0.1.0/pyproject.toml.orig +48 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/__init__.py +3 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/cli.py +84 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/jev_rank.py +60 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/model.py +11 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/util_detect_harness.py +51 -0
- dynamic_skill_loader-0.1.0/src/dynamic_skill_loader/util_read_skill.py +42 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aimmetal-tech
|
|
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,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dynamic-skill-loader
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dynamic skill loader with Jev ranking
|
|
5
|
+
Author: aimmetal-tech
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: mcp[cli]>=2.2.0
|
|
9
|
+
Requires-Dist: pydantic>=2.13.5
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
11
|
+
Requires-Dist: python-frontmatter>=1.3.0
|
|
12
|
+
Requires-Dist: typesafe-sdk>=0.7.0
|
|
13
|
+
Requires-Python: >=3.12, <3.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
<div align="center">
|
|
17
|
+
|
|
18
|
+
# Dynamic Skill Loader
|
|
19
|
+
|
|
20
|
+
[](https://www.python.org/)
|
|
21
|
+
[](https://docs.astral.sh/uv/)
|
|
22
|
+
[](https://modelcontextprotocol.io/)
|
|
23
|
+
[](https://typesafe.ai/)
|
|
24
|
+
[](https://docs.astral.sh/ruff/)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
|
|
27
|
+
An MCP server that discovers local skills and ranks them against a user's request with TypeSafe.
|
|
28
|
+
|
|
29
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
Dynamic Skill Loader is an MCP server that exposes a `retrieve_skills` tool. Given keywords and an optional original request, it:
|
|
36
|
+
|
|
37
|
+
1. Detects the connected MCP client's Harness.
|
|
38
|
+
2. Selects a skill root, honoring `--skills-dir` before Harness defaults.
|
|
39
|
+
3. Reads each skill's YAML front matter.
|
|
40
|
+
4. Uses TypeSafe to estimate relevance.
|
|
41
|
+
5. Returns `SkillScoreModel` values sorted by descending score.
|
|
42
|
+
|
|
43
|
+
Each result includes the skill name, relevance score, and an absolute path to its `SKILL.md` file. A client can use that path to read the full skill instructions.
|
|
44
|
+
|
|
45
|
+
The default skill roots are `~/.codex/skills` for Codex, `~/.claude/skills` for Claude Code, and `~/.agents/skills` for other Harness values. Use `--skills-dir` to select any compatible skill directory explicitly.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.12
|
|
50
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
51
|
+
- A TypeSafe API key for live ranking
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
For local development:
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
uv sync
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For end users, `uvx` can install and run the published package on demand. Pass `TYPESAFE_API_KEY` and optional `--skills-dir` after the package entry point.
|
|
62
|
+
|
|
63
|
+
## Run with uvx
|
|
64
|
+
|
|
65
|
+
An MCP client can launch the published package with:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": [
|
|
71
|
+
"dynamic-skill-loader",
|
|
72
|
+
"--TYPESAFE_API_KEY",
|
|
73
|
+
"your-typesafe-api-key",
|
|
74
|
+
"--skills-dir",
|
|
75
|
+
"~/.agents/skills"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The equivalent command line is:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
uvx dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ~/.agents/skills
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To run a local checkout instead:
|
|
87
|
+
|
|
88
|
+
```powershell
|
|
89
|
+
uv run dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ./skills
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--skills-dir` has the highest priority. `TYPESAFE_BASE_URL` remains an environment variable for non-default TypeSafe endpoints. Avoid committing API keys to configuration files that are shared publicly.
|
|
93
|
+
|
|
94
|
+
The server exposes:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
retrieve_skills(keyword: list[str], original_request: str | null = null)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Example input:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"keyword": ["web search", "recent news"],
|
|
105
|
+
"original_request": "Find a skill for searching today's news."
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Add a skill
|
|
110
|
+
|
|
111
|
+
Create a direct child directory under the selected skill root with a `SKILL.md` file. The YAML front matter must include `name` and `description`:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
<skills-root>/my-skill/SKILL.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
---
|
|
119
|
+
name: my-skill
|
|
120
|
+
description: A concise description of when this skill should be used.
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# My Skill
|
|
124
|
+
|
|
125
|
+
Operational instructions for the skill go here.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The loader does not search nested skill directories. Keep detailed references beside the skill and link to them from `SKILL.md`. For local development, `--skills-dir ./skills` can be used explicitly.
|
|
129
|
+
|
|
130
|
+
## Project structure
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
dynamic-skill-loader/
|
|
134
|
+
├── src/dynamic_skill_loader/
|
|
135
|
+
│ ├── cli.py # MCP server, arguments, and retrieve_skills
|
|
136
|
+
│ ├── jev_rank.py # TypeSafe ranking workflow
|
|
137
|
+
│ ├── model.py # SkillScoreModel
|
|
138
|
+
│ ├── util_detect_harness.py # MCP client Harness detection
|
|
139
|
+
│ └── util_read_skill.py # Skill roots, discovery, and metadata parsing
|
|
140
|
+
├── tests/ # Focused unit tests
|
|
141
|
+
├── skills/ # Optional local development skill root
|
|
142
|
+
├── pyproject.toml # Package metadata and dynamic-skill-loader entry point
|
|
143
|
+
├── uv.lock # Locked dependencies
|
|
144
|
+
├── LICENSE # MIT license
|
|
145
|
+
├── README.md
|
|
146
|
+
└── README.zh-CN.md
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The published wheel contains the Python server. Skills are loaded from the selected external root at runtime rather than bundled into the wheel.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
Run the test suite and checks through `uv`:
|
|
154
|
+
|
|
155
|
+
```powershell
|
|
156
|
+
uv run pytest
|
|
157
|
+
uv run ruff check .
|
|
158
|
+
uv run ruff format --check .
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Tests stub TypeSafe and skill discovery, so they do not require credentials or network access. See [AGENTS.md](AGENTS.md) for architecture notes and project-specific conventions.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT License. See [LICENSE](LICENSE) for the full text.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Dynamic Skill Loader
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](https://docs.astral.sh/uv/)
|
|
7
|
+
[](https://modelcontextprotocol.io/)
|
|
8
|
+
[](https://typesafe.ai/)
|
|
9
|
+
[](https://docs.astral.sh/ruff/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
An MCP server that discovers local skills and ranks them against a user's request with TypeSafe.
|
|
13
|
+
|
|
14
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
Dynamic Skill Loader is an MCP server that exposes a `retrieve_skills` tool. Given keywords and an optional original request, it:
|
|
21
|
+
|
|
22
|
+
1. Detects the connected MCP client's Harness.
|
|
23
|
+
2. Selects a skill root, honoring `--skills-dir` before Harness defaults.
|
|
24
|
+
3. Reads each skill's YAML front matter.
|
|
25
|
+
4. Uses TypeSafe to estimate relevance.
|
|
26
|
+
5. Returns `SkillScoreModel` values sorted by descending score.
|
|
27
|
+
|
|
28
|
+
Each result includes the skill name, relevance score, and an absolute path to its `SKILL.md` file. A client can use that path to read the full skill instructions.
|
|
29
|
+
|
|
30
|
+
The default skill roots are `~/.codex/skills` for Codex, `~/.claude/skills` for Claude Code, and `~/.agents/skills` for other Harness values. Use `--skills-dir` to select any compatible skill directory explicitly.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.12
|
|
35
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
36
|
+
- A TypeSafe API key for live ranking
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
For local development:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
uv sync
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For end users, `uvx` can install and run the published package on demand. Pass `TYPESAFE_API_KEY` and optional `--skills-dir` after the package entry point.
|
|
47
|
+
|
|
48
|
+
## Run with uvx
|
|
49
|
+
|
|
50
|
+
An MCP client can launch the published package with:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"command": "uvx",
|
|
55
|
+
"args": [
|
|
56
|
+
"dynamic-skill-loader",
|
|
57
|
+
"--TYPESAFE_API_KEY",
|
|
58
|
+
"your-typesafe-api-key",
|
|
59
|
+
"--skills-dir",
|
|
60
|
+
"~/.agents/skills"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The equivalent command line is:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uvx dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ~/.agents/skills
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
To run a local checkout instead:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
uv run dynamic-skill-loader --TYPESAFE_API_KEY your-typesafe-api-key --skills-dir ./skills
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`--skills-dir` has the highest priority. `TYPESAFE_BASE_URL` remains an environment variable for non-default TypeSafe endpoints. Avoid committing API keys to configuration files that are shared publicly.
|
|
78
|
+
|
|
79
|
+
The server exposes:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
retrieve_skills(keyword: list[str], original_request: str | null = null)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Example input:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"keyword": ["web search", "recent news"],
|
|
90
|
+
"original_request": "Find a skill for searching today's news."
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Add a skill
|
|
95
|
+
|
|
96
|
+
Create a direct child directory under the selected skill root with a `SKILL.md` file. The YAML front matter must include `name` and `description`:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
<skills-root>/my-skill/SKILL.md
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
---
|
|
104
|
+
name: my-skill
|
|
105
|
+
description: A concise description of when this skill should be used.
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
# My Skill
|
|
109
|
+
|
|
110
|
+
Operational instructions for the skill go here.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The loader does not search nested skill directories. Keep detailed references beside the skill and link to them from `SKILL.md`. For local development, `--skills-dir ./skills` can be used explicitly.
|
|
114
|
+
|
|
115
|
+
## Project structure
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
dynamic-skill-loader/
|
|
119
|
+
├── src/dynamic_skill_loader/
|
|
120
|
+
│ ├── cli.py # MCP server, arguments, and retrieve_skills
|
|
121
|
+
│ ├── jev_rank.py # TypeSafe ranking workflow
|
|
122
|
+
│ ├── model.py # SkillScoreModel
|
|
123
|
+
│ ├── util_detect_harness.py # MCP client Harness detection
|
|
124
|
+
│ └── util_read_skill.py # Skill roots, discovery, and metadata parsing
|
|
125
|
+
├── tests/ # Focused unit tests
|
|
126
|
+
├── skills/ # Optional local development skill root
|
|
127
|
+
├── pyproject.toml # Package metadata and dynamic-skill-loader entry point
|
|
128
|
+
├── uv.lock # Locked dependencies
|
|
129
|
+
├── LICENSE # MIT license
|
|
130
|
+
├── README.md
|
|
131
|
+
└── README.zh-CN.md
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The published wheel contains the Python server. Skills are loaded from the selected external root at runtime rather than bundled into the wheel.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
Run the test suite and checks through `uv`:
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
uv run pytest
|
|
142
|
+
uv run ruff check .
|
|
143
|
+
uv run ruff format --check .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Tests stub TypeSafe and skill discovery, so they do not require credentials or network access. See [AGENTS.md](AGENTS.md) for architecture notes and project-specific conventions.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT License. See [LICENSE](LICENSE) for the full text.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dynamic-skill-loader"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dynamic skill loader with Jev ranking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12,<3.13"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"mcp[cli]>=2.2.0",
|
|
11
|
+
"pydantic>=2.13.5",
|
|
12
|
+
"pydantic-settings>=2.15.0",
|
|
13
|
+
"python-frontmatter>=1.3.0",
|
|
14
|
+
"typesafe-sdk>=0.7.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[project.authors]]
|
|
18
|
+
name = "aimmetal-tech"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
dynamic-skill-loader = "dynamic_skill_loader:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=9.1.1",
|
|
30
|
+
"ruff>=0.11",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
target-version = "py312"
|
|
35
|
+
line-length = 88
|
|
36
|
+
src = [
|
|
37
|
+
"src",
|
|
38
|
+
"tests",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.format]
|
|
42
|
+
quote-style = "double"
|
|
43
|
+
indent-style = "space"
|
|
44
|
+
line-ending = "auto"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = [
|
|
48
|
+
"E4",
|
|
49
|
+
"E7",
|
|
50
|
+
"E9",
|
|
51
|
+
"F",
|
|
52
|
+
"I",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.isort]
|
|
56
|
+
known-first-party = ["dynamic_skill_loader"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dynamic-skill-loader"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dynamic skill loader with Jev ranking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "aimmetal-tech" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.12,<3.13"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp[cli]>=2.2.0",
|
|
14
|
+
"pydantic>=2.13.5",
|
|
15
|
+
"pydantic-settings>=2.15.0",
|
|
16
|
+
"python-frontmatter>=1.3.0",
|
|
17
|
+
"typesafe-sdk>=0.7.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
dynamic-skill-loader = "dynamic_skill_loader:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=9.1.1",
|
|
30
|
+
"ruff>=0.11",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py312"
|
|
36
|
+
line-length = 88
|
|
37
|
+
src = ["src", "tests"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff.format]
|
|
40
|
+
quote-style = "double"
|
|
41
|
+
indent-style = "space"
|
|
42
|
+
line-ending = "auto"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint.isort]
|
|
48
|
+
known-first-party = ["dynamic_skill_loader"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
from mcp.server import MCPServer
|
|
9
|
+
from mcp.server.mcpserver import Context
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from dynamic_skill_loader.jev_rank import rank_by_state
|
|
13
|
+
from dynamic_skill_loader.model import SkillScoreModel
|
|
14
|
+
from dynamic_skill_loader.util_detect_harness import detect_harness
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.INFO,
|
|
18
|
+
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
|
19
|
+
stream=sys.stderr,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
mcp = MCPServer("my-mcp")
|
|
25
|
+
SKILLS_DIR: Path | None = None
|
|
26
|
+
|
|
27
|
+
STATE = """<KEYWORD>
|
|
28
|
+
{keyword}
|
|
29
|
+
</KEYWORD>
|
|
30
|
+
|
|
31
|
+
<ORIGINAL_REQUEST>
|
|
32
|
+
{original_request}
|
|
33
|
+
</ORIGINAL_REQUEST>
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_state(keyword, original_request) -> str:
|
|
38
|
+
return STATE.format(keyword=", ".join(keyword), original_request=original_request)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def retrieve_skills(
|
|
43
|
+
ctx: Context,
|
|
44
|
+
keyword: Annotated[
|
|
45
|
+
list[str],
|
|
46
|
+
Field(
|
|
47
|
+
description="The keyword of user's question or request, using to search relevant skills."
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
original_request: Annotated[
|
|
51
|
+
str | None, Field(description="The original request or question from user.")
|
|
52
|
+
] = None,
|
|
53
|
+
) -> dict[str, SkillScoreModel]:
|
|
54
|
+
"""Retrieve skills by keyword and original request"""
|
|
55
|
+
harness = detect_harness(ctx)
|
|
56
|
+
logger.info("Detected agent harness: %s", harness)
|
|
57
|
+
state = build_state(keyword, original_request)
|
|
58
|
+
answer = rank_by_state(state, harness, skills_dir=SKILLS_DIR)
|
|
59
|
+
return answer
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# @mcp.tool
|
|
63
|
+
# def open_skills(
|
|
64
|
+
# path: Annotated[str, Field(description="Absolute root path of the skill")],
|
|
65
|
+
# ):
|
|
66
|
+
# """ "Open skill by name"""
|
|
67
|
+
# skill_root_abs_path = Path(path)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
global SKILLS_DIR
|
|
72
|
+
|
|
73
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
74
|
+
parser.add_argument("--skills-dir", type=Path)
|
|
75
|
+
parser.add_argument("--TYPESAFE_API_KEY")
|
|
76
|
+
args, _ = parser.parse_known_args()
|
|
77
|
+
SKILLS_DIR = args.skills_dir
|
|
78
|
+
|
|
79
|
+
if args.TYPESAFE_API_KEY is not None:
|
|
80
|
+
os.environ["TYPESAFE_API_KEY"] = args.TYPESAFE_API_KEY
|
|
81
|
+
|
|
82
|
+
if SKILLS_DIR is not None:
|
|
83
|
+
logger.info("Using skills directory from --skills-dir: %s", SKILLS_DIR)
|
|
84
|
+
mcp.run()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from typesafe_sdk import Choice, TypeSafeClient
|
|
4
|
+
|
|
5
|
+
from dynamic_skill_loader.model import SkillScoreModel
|
|
6
|
+
from dynamic_skill_loader.util_detect_harness import AgentHarness
|
|
7
|
+
from dynamic_skill_loader.util_read_skill import (
|
|
8
|
+
find_skill_meta_files,
|
|
9
|
+
get_skill_root,
|
|
10
|
+
read_skill_metadata,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def rank_by_state(
|
|
15
|
+
state: str,
|
|
16
|
+
harness: AgentHarness = AgentHarness.UNKNOWN,
|
|
17
|
+
skills_dir: Path | None = None,
|
|
18
|
+
):
|
|
19
|
+
client = TypeSafeClient()
|
|
20
|
+
|
|
21
|
+
skills_path = find_skill_meta_files(
|
|
22
|
+
root=get_skill_root(harness, skills_dir=skills_dir)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
skills_criteria = {}
|
|
26
|
+
|
|
27
|
+
dict_model: dict[str, SkillScoreModel] = {}
|
|
28
|
+
|
|
29
|
+
for skill_path in skills_path:
|
|
30
|
+
meta_json_str = read_skill_metadata(skill_path)
|
|
31
|
+
skill_name = meta_json_str["name"]
|
|
32
|
+
skills_criteria[skill_name] = meta_json_str
|
|
33
|
+
|
|
34
|
+
dict_model[skill_name] = SkillScoreModel(
|
|
35
|
+
name=skill_name, score=0.0, abs_path=skill_path
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
response = client.system_one(
|
|
39
|
+
state=state,
|
|
40
|
+
questions={
|
|
41
|
+
"skill_relevant": Choice(
|
|
42
|
+
instructions="Is this skill relevant to the user's question/request?",
|
|
43
|
+
criteria=skills_criteria,
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
answer = response.choices["skill_relevant"]
|
|
49
|
+
ranking = sorted(answer.probabilities.items(), key=lambda kv: kv[1], reverse=True)
|
|
50
|
+
for name, score in ranking:
|
|
51
|
+
model = dict_model[name]
|
|
52
|
+
model.score = score
|
|
53
|
+
|
|
54
|
+
return dict(
|
|
55
|
+
sorted(dict_model.items(), key=lambda item: item[1].score, reverse=True)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
rank_by_state("搜索今日新闻")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SkillScoreModel(BaseModel):
|
|
7
|
+
name: str = Field(description="Skill's name.")
|
|
8
|
+
score: float = Field(
|
|
9
|
+
description="The score of the match between the skill and user's request. The higher the score is, the better the match."
|
|
10
|
+
)
|
|
11
|
+
abs_path: Path = Field(description="The absolute path of the skill")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from mcp.server.mcpserver import Context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentHarness(StrEnum):
|
|
7
|
+
CODEX = "codex"
|
|
8
|
+
CLAUDE_CODE = "claude-code"
|
|
9
|
+
PI = "pi"
|
|
10
|
+
OMP = "omp"
|
|
11
|
+
GITHUB_COPILOT = "github-copilot"
|
|
12
|
+
UNKNOWN = "unknown"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HARNESS_MAP = {
|
|
16
|
+
"codex-mcp-client": AgentHarness.CODEX,
|
|
17
|
+
"codex": AgentHarness.CODEX,
|
|
18
|
+
"codex-cli": AgentHarness.CODEX,
|
|
19
|
+
"openai-codex": AgentHarness.CODEX,
|
|
20
|
+
"claude-code": AgentHarness.CLAUDE_CODE,
|
|
21
|
+
"claude-ai": AgentHarness.CLAUDE_CODE,
|
|
22
|
+
"claude": AgentHarness.CLAUDE_CODE,
|
|
23
|
+
"claude-code-cli": AgentHarness.CLAUDE_CODE,
|
|
24
|
+
"omp-coding-agent": AgentHarness.OMP,
|
|
25
|
+
"oh-my-pi": AgentHarness.OMP,
|
|
26
|
+
"pi": AgentHarness.PI,
|
|
27
|
+
"pi-cli": AgentHarness.PI,
|
|
28
|
+
"pi-coding-agent": AgentHarness.PI,
|
|
29
|
+
"github-copilot-developer": AgentHarness.GITHUB_COPILOT,
|
|
30
|
+
"copilot-cli": AgentHarness.GITHUB_COPILOT,
|
|
31
|
+
"github-copilot": AgentHarness.GITHUB_COPILOT,
|
|
32
|
+
"github-copilot-vscode": AgentHarness.GITHUB_COPILOT,
|
|
33
|
+
"visual-studio-code": AgentHarness.GITHUB_COPILOT,
|
|
34
|
+
"vscode": AgentHarness.GITHUB_COPILOT,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_harness(ctx: Context) -> AgentHarness:
|
|
39
|
+
client_info = getattr(
|
|
40
|
+
getattr(ctx.session, "client_params", None),
|
|
41
|
+
"client_info",
|
|
42
|
+
None,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
name = getattr(client_info, "name", "")
|
|
46
|
+
normalized_name = "-".join(name.strip().casefold().replace("_", "-").split())
|
|
47
|
+
|
|
48
|
+
return HARNESS_MAP.get(
|
|
49
|
+
normalized_name,
|
|
50
|
+
AgentHarness.UNKNOWN,
|
|
51
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import frontmatter
|
|
4
|
+
|
|
5
|
+
from dynamic_skill_loader.util_detect_harness import AgentHarness
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_skill_root(
|
|
9
|
+
harness: AgentHarness,
|
|
10
|
+
home: Path | None = None,
|
|
11
|
+
skills_dir: Path | None = None,
|
|
12
|
+
) -> Path:
|
|
13
|
+
if skills_dir is not None:
|
|
14
|
+
return skills_dir.expanduser()
|
|
15
|
+
|
|
16
|
+
home = Path.home() if home is None else home
|
|
17
|
+
|
|
18
|
+
if harness is AgentHarness.CODEX:
|
|
19
|
+
return home / ".codex" / "skills"
|
|
20
|
+
if harness is AgentHarness.CLAUDE_CODE:
|
|
21
|
+
return home / ".claude" / "skills"
|
|
22
|
+
return home / ".agents" / "skills"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_skill_meta_files(root: Path) -> list[Path]:
|
|
26
|
+
if not root.is_dir():
|
|
27
|
+
raise NotADirectoryError(f"不是有效目录:{root}")
|
|
28
|
+
root = root.resolve()
|
|
29
|
+
return sorted(root.glob("*/SKILL.md"), key=lambda p: p.parent.name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_skill_metadata(path: Path):
|
|
33
|
+
post = frontmatter.load(path)
|
|
34
|
+
return post.metadata
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
skills_path = find_skill_meta_files(root=Path("skills"))
|
|
39
|
+
for skill_path in skills_path:
|
|
40
|
+
print(skill_path)
|
|
41
|
+
meta = read_skill_metadata(Path("skills/tavily-search/SKILL.md"))
|
|
42
|
+
print(meta["name"])
|