agent-as-model 0.1.0__py3-none-any.whl

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,3 @@
1
+ """agent-as-model: a true MCP server where the agent is the model and the API key."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Run the agent-as-model MCP server: python -m agent_as_model."""
2
+
3
+ from agent_as_model.server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,95 @@
1
+ """Message translation for agent-as-model.
2
+
3
+ The server never touches a model API key. Every completion is produced by the
4
+ host agent's own LLM through MCP sampling (or the multi-round-trip successor
5
+ on 2026-07-28+ connections). This module translates the two most common
6
+ product API shapes into MCP sampling messages and extracts plain text from
7
+ sampling results.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal
13
+
14
+ from mcp.types import CreateMessageResult, SamplingMessage, TextContent
15
+ from pydantic import BaseModel
16
+
17
+
18
+ class ChatMessage(BaseModel):
19
+ """One OpenAI chat-completions message (text content only, v1)."""
20
+
21
+ role: Literal["user", "assistant", "system"]
22
+ content: str
23
+
24
+
25
+ class AnthropicContentBlock(BaseModel):
26
+ """One Anthropic text content block (v1 supports text only)."""
27
+
28
+ type: Literal["text"]
29
+ text: str
30
+
31
+
32
+ class AnthropicMessage(BaseModel):
33
+ """One Anthropic messages API message (text only, v1)."""
34
+
35
+ role: Literal["user", "assistant"]
36
+ content: str | list[AnthropicContentBlock]
37
+
38
+
39
+ def _text_of(content: str | list[AnthropicContentBlock]) -> str:
40
+ if isinstance(content, str):
41
+ return content
42
+ return "\n".join(block.text for block in content)
43
+
44
+
45
+ def openai_to_sampling(
46
+ messages: list[ChatMessage],
47
+ ) -> tuple[str | None, list[SamplingMessage]]:
48
+ """Split OpenAI chat messages into a system prompt and sampling messages."""
49
+ if not messages:
50
+ raise ValueError("messages must be a non-empty list")
51
+ system_parts: list[str] = []
52
+ sampling: list[SamplingMessage] = []
53
+ for message in messages:
54
+ if message.role == "system":
55
+ system_parts.append(message.content)
56
+ else:
57
+ sampling.append(
58
+ SamplingMessage(
59
+ role=message.role,
60
+ content=TextContent(type="text", text=message.content),
61
+ )
62
+ )
63
+ system_prompt = "\n\n".join(system_parts) if system_parts else None
64
+ return system_prompt, sampling
65
+
66
+
67
+ def anthropic_to_sampling(
68
+ messages: list[AnthropicMessage],
69
+ system: str | list[AnthropicContentBlock] | None,
70
+ ) -> tuple[str | None, list[SamplingMessage]]:
71
+ """Split an Anthropic messages request into a system prompt and sampling messages."""
72
+ if not messages:
73
+ raise ValueError("messages must be a non-empty list")
74
+ system_prompt = _text_of(system) if system is not None else None
75
+ sampling = [
76
+ SamplingMessage(
77
+ role=message.role,
78
+ content=TextContent(type="text", text=_text_of(message.content)),
79
+ )
80
+ for message in messages
81
+ ]
82
+ return system_prompt, sampling
83
+
84
+
85
+ def extract_text(result: CreateMessageResult) -> str:
86
+ """Pull plain text out of a sampling result.
87
+
88
+ Raises ValueError on non-text content instead of silently stringifying it.
89
+ """
90
+ content = result.content
91
+ if isinstance(content, TextContent):
92
+ return content.text
93
+ raise ValueError(
94
+ f"agent-as-model serves text completions only, the host returned {type(content).__name__}"
95
+ )
@@ -0,0 +1,162 @@
1
+ """agent-as-model: a true MCP server where the agent IS the model and the API key.
2
+
3
+ Install on any MCP host (Claude Code, Cursor, Windsurf, ...). The server
4
+ exposes product-style completion tools; every call is fulfilled by sampling
5
+ the host agent's own LLM. No API keys exist anywhere in this system.
6
+
7
+ Tools:
8
+ chat_complete OpenAI chat-completions-shaped request -> completion text
9
+ messages_create Anthropic messages-shaped request -> completion text
10
+ list_models the model this server offers (the host's own model, aliased)
11
+
12
+ Upstream note: sampling is deprecated as of the 2026-07-28 spec (SEP-2577,
13
+ earliest removal 2027-07-28). This server uses the era-portable
14
+ Resolve/Sample form: a direct sampling request on pre-2026 connections and
15
+ the multi-round-trip pattern on 2026-07-28+. One tool body serves both eras.
16
+ The host must declare the sampling capability or calls fail with a clear
17
+ MISSING_REQUIRED_CLIENT_CAPABILITY error.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from typing import Annotated
24
+
25
+ from mcp.server.mcpserver import MCPServer, Resolve, Sample
26
+ from mcp.types import (
27
+ CreateMessageResult,
28
+ ModelHint,
29
+ ModelPreferences,
30
+ )
31
+ from pydantic import Field
32
+
33
+ from agent_as_model import __version__
34
+ from agent_as_model.protocol import (
35
+ AnthropicContentBlock,
36
+ AnthropicMessage,
37
+ ChatMessage,
38
+ anthropic_to_sampling,
39
+ extract_text,
40
+ openai_to_sampling,
41
+ )
42
+
43
+ mcp = MCPServer(
44
+ name="agent-as-model",
45
+ version=__version__,
46
+ instructions=(
47
+ "Completion tools backed by the host agent's own model. "
48
+ "No API keys are used or needed: the connected agent is the model."
49
+ ),
50
+ )
51
+
52
+
53
+ async def _sample_chat(
54
+ messages: list[ChatMessage],
55
+ model: str,
56
+ temperature: float | None,
57
+ max_tokens: int,
58
+ ) -> Sample:
59
+ system_prompt, sampling_messages = openai_to_sampling(messages)
60
+ return Sample(
61
+ messages=sampling_messages,
62
+ system_prompt=system_prompt,
63
+ max_tokens=max_tokens,
64
+ temperature=temperature,
65
+ model_preferences=ModelPreferences(hints=[ModelHint(name=model)]),
66
+ )
67
+
68
+
69
+ @mcp.tool(
70
+ description=(
71
+ "Generate a chat completion from an OpenAI chat-completions-shaped request. "
72
+ "The completion is produced by the host agent's own model via MCP sampling. "
73
+ "No API key is used or needed: the connected agent is the model."
74
+ )
75
+ )
76
+ async def chat_complete(
77
+ completion: Annotated[CreateMessageResult, Resolve(_sample_chat)],
78
+ messages: Annotated[
79
+ list[ChatMessage],
80
+ Field(description="Chat messages; system roles become the system prompt."),
81
+ ],
82
+ model: Annotated[
83
+ str, Field(description="Model alias; agent-default means the host's own model.")
84
+ ] = "agent-default",
85
+ temperature: Annotated[
86
+ float | None, Field(description="Sampling temperature, if the host honors it.")
87
+ ] = None,
88
+ max_tokens: Annotated[int, Field(description="Maximum tokens to generate.")] = 4096,
89
+ ) -> str:
90
+ """Complete a chat with the host agent's model (OpenAI request shape)."""
91
+ return extract_text(completion)
92
+
93
+
94
+ async def _sample_messages(
95
+ messages: list[AnthropicMessage],
96
+ system: str | list[AnthropicContentBlock] | None,
97
+ model: str,
98
+ temperature: float | None,
99
+ max_tokens: int,
100
+ ) -> Sample:
101
+ system_prompt, sampling_messages = anthropic_to_sampling(messages, system)
102
+ return Sample(
103
+ messages=sampling_messages,
104
+ system_prompt=system_prompt,
105
+ max_tokens=max_tokens,
106
+ temperature=temperature,
107
+ model_preferences=ModelPreferences(hints=[ModelHint(name=model)]),
108
+ )
109
+
110
+
111
+ @mcp.tool(
112
+ description=(
113
+ "Generate a completion from an Anthropic messages-shaped request. "
114
+ "The completion is produced by the host agent's own model via MCP sampling. "
115
+ "No API key is used or needed: the connected agent is the model."
116
+ )
117
+ )
118
+ async def messages_create(
119
+ completion: Annotated[CreateMessageResult, Resolve(_sample_messages)],
120
+ messages: Annotated[
121
+ list[AnthropicMessage], Field(description="User/assistant messages in order.")
122
+ ],
123
+ model: Annotated[
124
+ str, Field(description="Model alias; agent-default means the host's own model.")
125
+ ] = "agent-default",
126
+ system: Annotated[
127
+ str | list[AnthropicContentBlock] | None,
128
+ Field(description="System prompt as text or text blocks."),
129
+ ] = None,
130
+ temperature: Annotated[
131
+ float | None, Field(description="Sampling temperature, if the host honors it.")
132
+ ] = None,
133
+ max_tokens: Annotated[int, Field(description="Maximum tokens to generate.")] = 4096,
134
+ ) -> str:
135
+ """Complete a conversation with the host agent's model (Anthropic request shape)."""
136
+ return extract_text(completion)
137
+
138
+
139
+ @mcp.tool(
140
+ description="List the models this server offers. There is exactly one: agent-default, an alias for the host agent's own model."
141
+ )
142
+ async def list_models() -> str:
143
+ """List available models (the host's own model, aliased)."""
144
+ return json.dumps(
145
+ [
146
+ {
147
+ "id": "agent-default",
148
+ "owned_by": "host-agent",
149
+ "description": (
150
+ "Alias for the MCP host's own model, reached via sampling. No API key required."
151
+ ),
152
+ }
153
+ ]
154
+ )
155
+
156
+
157
+ def main() -> None:
158
+ mcp.run_stdio_async()
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-as-model
3
+ Version: 0.1.0
4
+ Summary: A true MCP server where the agent is the model and the API key.
5
+ Author: Fernando Garza
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fernandogarzaaa/agent-as-model
8
+ Project-URL: Repository, https://github.com/fernandogarzaaa/agent-as-model
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: mcp<3,>=2
13
+ Dynamic: license-file
14
+
15
+ # agent-as-model
16
+
17
+ A true MCP server where the agent **is** the model and the API key.
18
+
19
+ Install it on any MCP host (Claude Code, Cursor, Windsurf, any spec-compliant
20
+ client). It exposes product-style completion tools. Every call is fulfilled by
21
+ sampling the host agent's own LLM. No API keys exist anywhere in this system.
22
+
23
+ ## The idea
24
+
25
+ AI products usually demand a model API key before they do anything useful.
26
+ This server flips that around. The tools speak the request shapes products
27
+ already use, and the intelligence comes from the agent on the other end of
28
+ the MCP connection. The agent is the model. The agent is the key.
29
+
30
+ ## Install
31
+
32
+ Requires Python 3.10+ and a host that declares the MCP `sampling`
33
+ capability. No API keys, ever.
34
+
35
+ Run it straight from the repo with uvx (no install step):
36
+
37
+ ```bash
38
+ claude mcp add agent-as-model -- uvx --from git+https://github.com/fernandogarzaaa/agent-as-model agent-as-model
39
+ ```
40
+
41
+ Or install it for real:
42
+
43
+ ```bash
44
+ pipx install git+https://github.com/fernandogarzaaa/agent-as-model
45
+ # then
46
+ claude mcp add agent-as-model -- agent-as-model
47
+ ```
48
+
49
+ Cursor, Windsurf, or any client that takes MCP JSON:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "agent-as-model": {
55
+ "command": "uvx",
56
+ "args": ["--from", "git+https://github.com/fernandogarzaaa/agent-as-model", "agent-as-model"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ From a local checkout: `python -m agent_as_model`.
63
+
64
+ ## Tools
65
+
66
+ **chat_complete** — OpenAI chat-completions-shaped request, completion text back.
67
+
68
+ ```json
69
+ {
70
+ "messages": [
71
+ {"role": "system", "content": "You are terse."},
72
+ {"role": "user", "content": "Explain MCP sampling."}
73
+ ],
74
+ "model": "agent-default",
75
+ "temperature": 0.7,
76
+ "max_tokens": 500
77
+ }
78
+ ```
79
+
80
+ **messages_create** — Anthropic messages-shaped request, completion text back.
81
+ `system` accepts a string or a list of text blocks. Message content accepts a
82
+ string or a list of text blocks.
83
+
84
+ **list_models** — returns the one model this server offers: `agent-default`,
85
+ an alias for the host agent's own model.
86
+
87
+ ## How it works
88
+
89
+ 1. A product (or agent) calls `chat_complete` with ordinary chat messages.
90
+ 2. The server translates them into MCP sampling messages and asks the host
91
+ to complete them. On pre-2026 connections this is a direct
92
+ `sampling/createMessage` request. On 2026-07-28+ it rides the
93
+ multi-round-trip pattern. One tool body serves both eras.
94
+ 3. The host agent generates the completion with its own model and the server
95
+ returns the text. The `model` argument is passed to the host as a model
96
+ hint only; `agent-default` means "your own model".
97
+
98
+ ## Honest caveats
99
+
100
+ - Sampling is deprecated upstream (SEP-2577, spec 2026-07-28, earliest
101
+ removal 2027-07-28). The official migration path is provider API keys.
102
+ This project exists because the agent-as-model architecture is the point,
103
+ not the keys. The era-portable form used here is the sanctioned way to
104
+ keep it working across both spec eras.
105
+ - The host is in charge. Sampling runs under the host's consent and policy.
106
+ A host without the sampling capability gets a clear
107
+ `MISSING_REQUIRED_CLIENT_CAPABILITY` error, not a silent failure.
108
+ - Latency is one host model call per tool call. This is a thinking
109
+ endpoint, not a fast one.
110
+ - v1 serves text only. Tool calls, images, and audio in either direction
111
+ are rejected with explicit errors, not silently dropped.
112
+
113
+ ## Roadmap
114
+
115
+ - Provider-key fallback for hosts without sampling (opt-in, per-call choice
116
+ stays with the user).
117
+ - Structured output passthrough where hosts support it.
118
+ - More product shapes as tools (embeddings-shaped requests cannot be
119
+ sampled meaningfully; out of scope by design).
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ python3 -m pytest tests/ -q
125
+ python3 -m ruff format . && python3 -m ruff check .
126
+ ```
127
+
128
+ 14 tests, all green: message translation unit tests plus end-to-end runs of
129
+ the full MCP loop (tool call to sampled completion) against a scripted host
130
+ on both the modern multi-round-trip era and the legacy direct-sampling era.
@@ -0,0 +1,10 @@
1
+ agent_as_model/__init__.py,sha256=DkLGJdKQjReesdGnBVoRMaHJYZ_52PboaNNgfdC0SVY,109
2
+ agent_as_model/__main__.py,sha256=GPs1-oXObYgUM_MLZGYf-ejXgGOQ34nc2xkMYaGdDj8,146
3
+ agent_as_model/protocol.py,sha256=iA0u6gofCVSCFIP2SkbytHFYilZHkhdBp8TegB0bv9I,3071
4
+ agent_as_model/server.py,sha256=8y2IrLny7Q6VxOHxCQSy5Du1WGmkvggmeyaVU-hE8iM,5407
5
+ agent_as_model-0.1.0.dist-info/licenses/LICENSE,sha256=VbqRYzR3eEfKB0qZfOnFyBS3eKnLpi_qjzqqkD7o028,1071
6
+ agent_as_model-0.1.0.dist-info/METADATA,sha256=7kCPJMcccCaiHvvqjvz8Z1_NBf8bUrIePqEOZppYX1w,4423
7
+ agent_as_model-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ agent_as_model-0.1.0.dist-info/entry_points.txt,sha256=UWxSor6R970wuPCtSooNp8kCCpMo_olPykJFzeITkX0,62
9
+ agent_as_model-0.1.0.dist-info/top_level.txt,sha256=9TogFQ2wHtPGqeRkYQD9gplJXFGIV8-FaX-j8rCf1gY,15
10
+ agent_as_model-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-as-model = agent_as_model.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fernando Garza
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 @@
1
+ agent_as_model