agent-card-starter 0.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 flowboxx
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,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-card-starter
3
+ Version: 0.2.0
4
+ Summary: Make a small business readable for AI assistants: write and check an A2A agent card or MCP server card. Standard library only.
5
+ Author-email: flowboxx <kontakt@flowboxx.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://flowboxx.de/
8
+ Project-URL: Source, https://github.com/flowboxx/agent-card-starter
9
+ Project-URL: Issues, https://github.com/flowboxx/agent-card-starter/issues
10
+ Keywords: a2a,agent-card,mcp,mcp-server-card,well-known,small-business
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # agent-card-starter
21
+
22
+ [![tests](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml)
23
+ ![license MIT](https://img.shields.io/badge/license-MIT-blue)
24
+ ![python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
25
+
26
+ **In 60 seconds:** AI assistants read a small JSON file to learn what your business does. This tool writes that file for you and checks it. Offline, no account, no dependencies.
27
+
28
+ ```bash
29
+ pip install git+https://github.com/flowboxx/agent-card-starter
30
+ agent-card init --name "Muster Roofing Ltd" --description "Roofer in Musterstadt, repairs and new roofs." --website https://www.example.com --skill "Flat roof sealing"
31
+ agent-card validate .well-known/agent-card.json --strict
32
+ ```
33
+
34
+ Upload the file to `https://<your-domain>/.well-known/agent-card.json` and you are readable. Nothing here guarantees that an assistant will mention you.
35
+
36
+ **In 60 Sekunden:** KI-Assistenten lesen eine kleine Datei, um zu erfahren, was Ihr Betrieb macht. Dieses Werkzeug schreibt die Datei und prüft sie. Ohne Netz, ohne Konto, ohne Fremdpakete. Befehle siehe oben, die Datei kommt nach `https://<Ihre-Domain>/.well-known/agent-card.json`.
37
+
38
+ <!-- Picture idea: a 20 second terminal recording (init, then validate with OK) of the three commands above. Fictional business only. -->
39
+
40
+ Starter kit to make a small business readable and reachable for AI assistants:
41
+ an A2A agent card, an MCP server card, a minimal MCP example server and a validator.
42
+ Standard library only, Python 3.9+, MIT licensed.
43
+
44
+ Starter-Set, damit ein kleiner Betrieb für KI-Assistenten lesbar und erreichbar wird:
45
+ Agentenkarte (A2A), Server-Karte (MCP), ein minimaler MCP-Beispielserver und ein Prüfskript.
46
+ Nur Python-Standardbibliothek, ab Python 3.9, MIT-Lizenz.
47
+
48
+ Maintained by [flowboxx](https://flowboxx.de/?ref=github), an agency that builds AI doors for businesses.
49
+ Gepflegt von [flowboxx](https://flowboxx.de/?ref=github), einer Agentur, die KI-Türen für Betriebe baut.
50
+
51
+ ---
52
+
53
+ ## English
54
+
55
+ ### What is in here
56
+
57
+ | Path | What it is |
58
+ | --- | --- |
59
+ | `examples/well-known/agent-card.json` | A2A agent card for a fictional "Beispiel Dachdecker GmbH" |
60
+ | `examples/well-known/agent.json` | Byte-identical copy for older readers |
61
+ | `examples/agent-card.annotated.jsonc` | Same card with comments (documentation only) |
62
+ | `examples/mcp-server-card.json` | MCP server card for the same business |
63
+ | `templates/` | Blank-ish templates: server card and AI catalog |
64
+ | `server/mcp_server.py` | MCP example server over stdio: tools `leistungen` and `anfrage_anlegen` (stub) |
65
+ | `agent_card/` | The `agent-card` command: `init` writes a first card, `validate` checks card, server card and catalog (offline) |
66
+ | `tools/validate_agent_card.py` | Same validator as a plain script |
67
+ | `tests/` | pytest suite |
68
+
69
+ ### Quick start
70
+
71
+ ```bash
72
+ agent-card init --name "Your Business" --description "What you do and where." --website https://www.example.com
73
+ agent-card validate .well-known/agent-card.json --strict
74
+ # from a clone, without installing:
75
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
76
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
77
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
78
+ python3 -m pip install pytest && python3 -m pytest -q
79
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
80
+ ```
81
+
82
+ ### Use it for your business
83
+
84
+ 1. Copy `examples/well-known/agent-card.json`, replace name, description, provider, skills.
85
+ 2. List only services you really offer. One skill per service, stable ids.
86
+ 3. Keep prices, phone numbers and promises out of the card. A human answers those.
87
+ 4. Run the validator, publish the file at `https://<your-domain>/.well-known/agent-card.json`.
88
+ 5. Point `supportedInterfaces` at an endpoint you really operate. The example server
89
+ speaks MCP over stdio for learning. A public endpoint needs HTTP transport,
90
+ rate limiting, spam protection and a privacy notice. `anfrage_anlegen` is a stub on purpose.
91
+
92
+ ### Notes on the standards
93
+
94
+ - The agent card follows the A2A v1.0 field names (`supportedInterfaces`, `skills`, `provider`).
95
+ - The server card follows the MCP Server Card proposal (SEP-2127). It is an extension and
96
+ the schema may still change. Check the current schema before you publish.
97
+ - The validator checks required fields and a few sanity rules. It is not a full JSON Schema validation.
98
+ - Nothing here guarantees that an assistant will mention or recommend you.
99
+
100
+ ---
101
+
102
+ ## Deutsch
103
+
104
+ ### Was drin ist
105
+
106
+ | Pfad | Inhalt |
107
+ | --- | --- |
108
+ | `examples/well-known/agent-card.json` | A2A-Agentenkarte für die fiktive "Beispiel Dachdecker GmbH" |
109
+ | `examples/well-known/agent.json` | Byte-gleiche Kopie für ältere Leser |
110
+ | `examples/agent-card.annotated.jsonc` | Dieselbe Karte mit Kommentaren (nur zur Erklärung) |
111
+ | `examples/mcp-server-card.json` | MCP-Server-Karte für denselben Betrieb |
112
+ | `templates/` | Vorlagen: Server-Karte und KI-Katalog |
113
+ | `server/mcp_server.py` | MCP-Beispielserver über stdio: Werkzeuge `leistungen` und `anfrage_anlegen` (Stub) |
114
+ | `agent_card/` | Der Befehl `agent-card`: `init` schreibt eine erste Karte, `validate` prüft Karte, Server-Karte und Katalog (ohne Netz) |
115
+ | `tools/validate_agent_card.py` | Dasselbe Prüfskript als einfache Datei |
116
+ | `tests/` | pytest-Tests |
117
+
118
+ ### Schnellstart
119
+
120
+ ```bash
121
+ agent-card init --name "Ihr Betrieb" --description "Was Sie tun und wo." --website https://www.example.com
122
+ agent-card validate .well-known/agent-card.json --strict
123
+ # aus einem Klon, ohne Installation:
124
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
125
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
126
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
127
+ python3 -m pip install pytest && python3 -m pytest -q
128
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
129
+ ```
130
+
131
+ ### Für den eigenen Betrieb nutzen
132
+
133
+ 1. `examples/well-known/agent-card.json` kopieren, Name, Beschreibung, Anbieter und Fähigkeiten ersetzen.
134
+ 2. Nur Leistungen eintragen, die Sie wirklich anbieten. Eine Fähigkeit je Leistung, stabile Ids.
135
+ 3. Keine Preise, Telefonnummern oder Zusagen in die Karte. Das beantwortet ein Mensch.
136
+ 4. Prüfskript laufen lassen, Datei unter `https://<Ihre-Domain>/.well-known/agent-card.json` ablegen.
137
+ 5. `supportedInterfaces` auf einen Endpunkt zeigen lassen, den Sie wirklich betreiben. Der Beispielserver
138
+ spricht MCP über stdio zum Lernen. Ein öffentlicher Endpunkt braucht HTTP, Mengenbegrenzung,
139
+ Spam-Schutz und einen Datenschutzhinweis. `anfrage_anlegen` ist absichtlich ein Stub und speichert nichts.
140
+
141
+ ### Hinweise zu den Standards
142
+
143
+ - Die Agentenkarte folgt den Feldnamen von A2A v1.0 (`supportedInterfaces`, `skills`, `provider`).
144
+ - Die Server-Karte folgt dem MCP-Vorschlag Server Card (SEP-2127). Das ist eine Erweiterung,
145
+ das Schema kann sich noch ändern. Vor dem Veröffentlichen das aktuelle Schema prüfen.
146
+ - Das Prüfskript prüft Pflichtfelder und einige Plausibilitäten, keine vollständige JSON-Schema-Validierung.
147
+ - Nichts hier garantiert, dass ein Assistent Sie nennt oder empfiehlt.
148
+
149
+ ---
150
+
151
+ ## Built with this? Tell us / Damit gebaut? Erzählen Sie es uns
152
+
153
+ If you published a card with this kit, show it in [Discussions, Show and tell](../../discussions/categories/show-and-tell). We read every one. If you like, add the badge to your README:
154
+
155
+ Wenn Sie mit diesem Kit eine Karte veröffentlicht haben, zeigen Sie sie unter [Discussions, Show and tell](../../discussions/categories/show-and-tell). Das Abzeichen für Ihre README:
156
+
157
+ ```markdown
158
+ [![built with agent-card-starter](https://img.shields.io/badge/built%20with-agent--card--starter-2b6cb0)](https://github.com/flowboxx/agent-card-starter)
159
+ ```
160
+
161
+ ---
162
+
163
+ ## As is / Ohne Gewähr
164
+
165
+ This is a starting point, provided "as is" under the MIT license, without warranty of any kind. Standards around agent cards and server cards are young and change. Check the current specifications before you publish. Nothing here is legal advice, and nothing guarantees that an assistant will mention or recommend your business. Changes are listed in [CHANGELOG.md](CHANGELOG.md), how to help is in [CONTRIBUTING.md](CONTRIBUTING.md).
166
+
167
+ Dies ist ein Ausgangspunkt, bereitgestellt "wie besehen" unter der MIT-Lizenz, ohne Gewähr. Die Standards rund um Agentenkarten und Server-Karten sind jung und ändern sich. Prüfen Sie vor dem Veröffentlichen die aktuellen Spezifikationen. Nichts hier ist Rechtsberatung, und nichts garantiert, dass ein Assistent Ihren Betrieb nennt oder empfiehlt. Änderungen stehen in [CHANGELOG.md](CHANGELOG.md), Mitmachen steht in [CONTRIBUTING.md](CONTRIBUTING.md).
168
+
169
+ MIT License, see `LICENSE`. Questions and contributions welcome via issues. Fragen und Beiträge gern als Issue.
170
+
171
+ Built and maintained by [flowboxx](https://flowboxx.de/?ref=github), Germany.
@@ -0,0 +1,152 @@
1
+ # agent-card-starter
2
+
3
+ [![tests](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml)
4
+ ![license MIT](https://img.shields.io/badge/license-MIT-blue)
5
+ ![python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
6
+
7
+ **In 60 seconds:** AI assistants read a small JSON file to learn what your business does. This tool writes that file for you and checks it. Offline, no account, no dependencies.
8
+
9
+ ```bash
10
+ pip install git+https://github.com/flowboxx/agent-card-starter
11
+ agent-card init --name "Muster Roofing Ltd" --description "Roofer in Musterstadt, repairs and new roofs." --website https://www.example.com --skill "Flat roof sealing"
12
+ agent-card validate .well-known/agent-card.json --strict
13
+ ```
14
+
15
+ Upload the file to `https://<your-domain>/.well-known/agent-card.json` and you are readable. Nothing here guarantees that an assistant will mention you.
16
+
17
+ **In 60 Sekunden:** KI-Assistenten lesen eine kleine Datei, um zu erfahren, was Ihr Betrieb macht. Dieses Werkzeug schreibt die Datei und prüft sie. Ohne Netz, ohne Konto, ohne Fremdpakete. Befehle siehe oben, die Datei kommt nach `https://<Ihre-Domain>/.well-known/agent-card.json`.
18
+
19
+ <!-- Picture idea: a 20 second terminal recording (init, then validate with OK) of the three commands above. Fictional business only. -->
20
+
21
+ Starter kit to make a small business readable and reachable for AI assistants:
22
+ an A2A agent card, an MCP server card, a minimal MCP example server and a validator.
23
+ Standard library only, Python 3.9+, MIT licensed.
24
+
25
+ Starter-Set, damit ein kleiner Betrieb für KI-Assistenten lesbar und erreichbar wird:
26
+ Agentenkarte (A2A), Server-Karte (MCP), ein minimaler MCP-Beispielserver und ein Prüfskript.
27
+ Nur Python-Standardbibliothek, ab Python 3.9, MIT-Lizenz.
28
+
29
+ Maintained by [flowboxx](https://flowboxx.de/?ref=github), an agency that builds AI doors for businesses.
30
+ Gepflegt von [flowboxx](https://flowboxx.de/?ref=github), einer Agentur, die KI-Türen für Betriebe baut.
31
+
32
+ ---
33
+
34
+ ## English
35
+
36
+ ### What is in here
37
+
38
+ | Path | What it is |
39
+ | --- | --- |
40
+ | `examples/well-known/agent-card.json` | A2A agent card for a fictional "Beispiel Dachdecker GmbH" |
41
+ | `examples/well-known/agent.json` | Byte-identical copy for older readers |
42
+ | `examples/agent-card.annotated.jsonc` | Same card with comments (documentation only) |
43
+ | `examples/mcp-server-card.json` | MCP server card for the same business |
44
+ | `templates/` | Blank-ish templates: server card and AI catalog |
45
+ | `server/mcp_server.py` | MCP example server over stdio: tools `leistungen` and `anfrage_anlegen` (stub) |
46
+ | `agent_card/` | The `agent-card` command: `init` writes a first card, `validate` checks card, server card and catalog (offline) |
47
+ | `tools/validate_agent_card.py` | Same validator as a plain script |
48
+ | `tests/` | pytest suite |
49
+
50
+ ### Quick start
51
+
52
+ ```bash
53
+ agent-card init --name "Your Business" --description "What you do and where." --website https://www.example.com
54
+ agent-card validate .well-known/agent-card.json --strict
55
+ # from a clone, without installing:
56
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
57
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
58
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
59
+ python3 -m pip install pytest && python3 -m pytest -q
60
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
61
+ ```
62
+
63
+ ### Use it for your business
64
+
65
+ 1. Copy `examples/well-known/agent-card.json`, replace name, description, provider, skills.
66
+ 2. List only services you really offer. One skill per service, stable ids.
67
+ 3. Keep prices, phone numbers and promises out of the card. A human answers those.
68
+ 4. Run the validator, publish the file at `https://<your-domain>/.well-known/agent-card.json`.
69
+ 5. Point `supportedInterfaces` at an endpoint you really operate. The example server
70
+ speaks MCP over stdio for learning. A public endpoint needs HTTP transport,
71
+ rate limiting, spam protection and a privacy notice. `anfrage_anlegen` is a stub on purpose.
72
+
73
+ ### Notes on the standards
74
+
75
+ - The agent card follows the A2A v1.0 field names (`supportedInterfaces`, `skills`, `provider`).
76
+ - The server card follows the MCP Server Card proposal (SEP-2127). It is an extension and
77
+ the schema may still change. Check the current schema before you publish.
78
+ - The validator checks required fields and a few sanity rules. It is not a full JSON Schema validation.
79
+ - Nothing here guarantees that an assistant will mention or recommend you.
80
+
81
+ ---
82
+
83
+ ## Deutsch
84
+
85
+ ### Was drin ist
86
+
87
+ | Pfad | Inhalt |
88
+ | --- | --- |
89
+ | `examples/well-known/agent-card.json` | A2A-Agentenkarte für die fiktive "Beispiel Dachdecker GmbH" |
90
+ | `examples/well-known/agent.json` | Byte-gleiche Kopie für ältere Leser |
91
+ | `examples/agent-card.annotated.jsonc` | Dieselbe Karte mit Kommentaren (nur zur Erklärung) |
92
+ | `examples/mcp-server-card.json` | MCP-Server-Karte für denselben Betrieb |
93
+ | `templates/` | Vorlagen: Server-Karte und KI-Katalog |
94
+ | `server/mcp_server.py` | MCP-Beispielserver über stdio: Werkzeuge `leistungen` und `anfrage_anlegen` (Stub) |
95
+ | `agent_card/` | Der Befehl `agent-card`: `init` schreibt eine erste Karte, `validate` prüft Karte, Server-Karte und Katalog (ohne Netz) |
96
+ | `tools/validate_agent_card.py` | Dasselbe Prüfskript als einfache Datei |
97
+ | `tests/` | pytest-Tests |
98
+
99
+ ### Schnellstart
100
+
101
+ ```bash
102
+ agent-card init --name "Ihr Betrieb" --description "Was Sie tun und wo." --website https://www.example.com
103
+ agent-card validate .well-known/agent-card.json --strict
104
+ # aus einem Klon, ohne Installation:
105
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
106
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
107
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
108
+ python3 -m pip install pytest && python3 -m pytest -q
109
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
110
+ ```
111
+
112
+ ### Für den eigenen Betrieb nutzen
113
+
114
+ 1. `examples/well-known/agent-card.json` kopieren, Name, Beschreibung, Anbieter und Fähigkeiten ersetzen.
115
+ 2. Nur Leistungen eintragen, die Sie wirklich anbieten. Eine Fähigkeit je Leistung, stabile Ids.
116
+ 3. Keine Preise, Telefonnummern oder Zusagen in die Karte. Das beantwortet ein Mensch.
117
+ 4. Prüfskript laufen lassen, Datei unter `https://<Ihre-Domain>/.well-known/agent-card.json` ablegen.
118
+ 5. `supportedInterfaces` auf einen Endpunkt zeigen lassen, den Sie wirklich betreiben. Der Beispielserver
119
+ spricht MCP über stdio zum Lernen. Ein öffentlicher Endpunkt braucht HTTP, Mengenbegrenzung,
120
+ Spam-Schutz und einen Datenschutzhinweis. `anfrage_anlegen` ist absichtlich ein Stub und speichert nichts.
121
+
122
+ ### Hinweise zu den Standards
123
+
124
+ - Die Agentenkarte folgt den Feldnamen von A2A v1.0 (`supportedInterfaces`, `skills`, `provider`).
125
+ - Die Server-Karte folgt dem MCP-Vorschlag Server Card (SEP-2127). Das ist eine Erweiterung,
126
+ das Schema kann sich noch ändern. Vor dem Veröffentlichen das aktuelle Schema prüfen.
127
+ - Das Prüfskript prüft Pflichtfelder und einige Plausibilitäten, keine vollständige JSON-Schema-Validierung.
128
+ - Nichts hier garantiert, dass ein Assistent Sie nennt oder empfiehlt.
129
+
130
+ ---
131
+
132
+ ## Built with this? Tell us / Damit gebaut? Erzählen Sie es uns
133
+
134
+ If you published a card with this kit, show it in [Discussions, Show and tell](../../discussions/categories/show-and-tell). We read every one. If you like, add the badge to your README:
135
+
136
+ Wenn Sie mit diesem Kit eine Karte veröffentlicht haben, zeigen Sie sie unter [Discussions, Show and tell](../../discussions/categories/show-and-tell). Das Abzeichen für Ihre README:
137
+
138
+ ```markdown
139
+ [![built with agent-card-starter](https://img.shields.io/badge/built%20with-agent--card--starter-2b6cb0)](https://github.com/flowboxx/agent-card-starter)
140
+ ```
141
+
142
+ ---
143
+
144
+ ## As is / Ohne Gewähr
145
+
146
+ This is a starting point, provided "as is" under the MIT license, without warranty of any kind. Standards around agent cards and server cards are young and change. Check the current specifications before you publish. Nothing here is legal advice, and nothing guarantees that an assistant will mention or recommend your business. Changes are listed in [CHANGELOG.md](CHANGELOG.md), how to help is in [CONTRIBUTING.md](CONTRIBUTING.md).
147
+
148
+ Dies ist ein Ausgangspunkt, bereitgestellt "wie besehen" unter der MIT-Lizenz, ohne Gewähr. Die Standards rund um Agentenkarten und Server-Karten sind jung und ändern sich. Prüfen Sie vor dem Veröffentlichen die aktuellen Spezifikationen. Nichts hier ist Rechtsberatung, und nichts garantiert, dass ein Assistent Ihren Betrieb nennt oder empfiehlt. Änderungen stehen in [CHANGELOG.md](CHANGELOG.md), Mitmachen steht in [CONTRIBUTING.md](CONTRIBUTING.md).
149
+
150
+ MIT License, see `LICENSE`. Questions and contributions welcome via issues. Fragen und Beiträge gern als Issue.
151
+
152
+ Built and maintained by [flowboxx](https://flowboxx.de/?ref=github), Germany.
@@ -0,0 +1,2 @@
1
+ """agent-card-starter: make a small business readable for AI assistants. Standard library only."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from agent_card.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,105 @@
1
+ """Command line: `agent-card validate FILE` and `agent-card init`. Standard library only."""
2
+ import argparse
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ from importlib import resources
8
+
9
+ from agent_card import __version__, validator
10
+
11
+
12
+ def _template() -> str:
13
+ return resources.files("agent_card").joinpath("data/agent-card.template.json").read_text(encoding="utf-8")
14
+
15
+
16
+ def _slug(text: str) -> str:
17
+ s = re.sub(r"[^a-z0-9]+", "-", text.lower().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")).strip("-")
18
+ return s or "leistung"
19
+
20
+
21
+ def skills_from(items) -> list:
22
+ """`--skill "Name"` or `--skill "Name: one sentence"`. Ids are made from the name and stay stable."""
23
+ out, seen = [], set()
24
+ for raw in items:
25
+ name, _, desc = raw.partition(":")
26
+ name, desc = name.strip(), desc.strip()
27
+ if not name:
28
+ continue
29
+ sid, n = _slug(name), 2
30
+ while sid in seen:
31
+ sid, n = "%s-%d" % (_slug(name), n), n + 1
32
+ seen.add(sid)
33
+ out.append({"id": sid, "name": name, "description": desc or name + ".", "tags": [_slug(name)]})
34
+ return out
35
+
36
+
37
+ def build_card(name: str, description: str, website: str, endpoint: str, skills=None) -> dict:
38
+ """Fill the template. Values go through json so quotes in names cannot break the file."""
39
+ card = json.loads(_template())
40
+
41
+ def fill(x):
42
+ if isinstance(x, str):
43
+ return x.replace("{name}", name).replace("{description}", description).replace("{website}", website).replace("{endpoint}", endpoint)
44
+ if isinstance(x, list):
45
+ return [fill(i) for i in x]
46
+ if isinstance(x, dict):
47
+ return {k: fill(v) for k, v in x.items()}
48
+ return x
49
+ card = fill(card)
50
+ if skills:
51
+ card["skills"] = skills + card["skills"] # the inquiry skill stays last
52
+ return card
53
+
54
+
55
+ def cmd_init(a) -> int:
56
+ website = a.website.rstrip("/")
57
+ endpoint = a.endpoint or website + "/mcp"
58
+ card = build_card(a.name, a.description, website, endpoint, skills_from(a.skill))
59
+ errors, warnings = validator.validate(card, "agent-card")
60
+ if errors:
61
+ for m in errors:
62
+ print("ERROR " + m)
63
+ return 1
64
+ target = os.path.join(a.out, ".well-known", "agent-card.json")
65
+ if os.path.exists(target) and not a.force:
66
+ print("ERROR %s exists already (use --force to overwrite)" % target)
67
+ return 1
68
+ os.makedirs(os.path.dirname(target), exist_ok=True)
69
+ with open(target, "w", encoding="utf-8") as f:
70
+ json.dump(card, f, ensure_ascii=False, indent=2)
71
+ f.write("\n")
72
+ for m in warnings:
73
+ print("WARN " + m)
74
+ print("Wrote %s" % target)
75
+ print("Next: add one skill per service you really offer, keep prices and phone numbers out,")
76
+ print("point supportedInterfaces at an endpoint you really operate, run `agent-card validate %s`," % target)
77
+ print("then publish the file at %s/.well-known/agent-card.json" % website)
78
+ return 0
79
+
80
+
81
+ def main(argv=None) -> int:
82
+ ap = argparse.ArgumentParser(prog="agent-card", description="Make a small business readable for AI assistants.")
83
+ ap.add_argument("--version", action="version", version="agent-card-starter " + __version__)
84
+ sub = ap.add_subparsers(dest="cmd", required=True)
85
+ v = sub.add_parser("validate", help="check an agent card, server card or catalog (offline)")
86
+ v.add_argument("file")
87
+ v.add_argument("--type", choices=["auto", "agent-card", "server-card", "catalog"], default="auto")
88
+ v.add_argument("--strict", action="store_true", help="treat warnings as errors")
89
+ i = sub.add_parser("init", help="write a first agent card to <out>/.well-known/agent-card.json")
90
+ i.add_argument("--name", required=True, help="business name")
91
+ i.add_argument("--description", required=True, help="one or two sentences: what you do and where")
92
+ i.add_argument("--website", required=True, help="https URL of your website")
93
+ i.add_argument("--endpoint", default="", help="https URL of an endpoint you really operate (default: <website>/mcp)")
94
+ i.add_argument("--skill", action="append", default=[], metavar="NAME[: SENTENCE]", help="one service you really offer, repeat for more")
95
+ i.add_argument("--out", default=".", help="folder to write into (default: current folder)")
96
+ i.add_argument("--force", action="store_true", help="overwrite an existing card")
97
+ a = ap.parse_args(argv)
98
+ if a.cmd == "validate":
99
+ extra = [a.file, "--type", a.type] + (["--strict"] if a.strict else [])
100
+ return validator.main(extra)
101
+ return cmd_init(a)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{name}",
3
+ "description": "{description}",
4
+ "version": "1.0.0",
5
+ "supportedInterfaces": [
6
+ {
7
+ "url": "{endpoint}",
8
+ "protocolBinding": "MCP",
9
+ "protocolVersion": "2025-06-18"
10
+ }
11
+ ],
12
+ "provider": {
13
+ "organization": "{name}",
14
+ "url": "{website}"
15
+ },
16
+ "documentationUrl": "{website}",
17
+ "capabilities": {
18
+ "streaming": false,
19
+ "pushNotifications": false
20
+ },
21
+ "defaultInputModes": ["application/json"],
22
+ "defaultOutputModes": ["application/json"],
23
+ "skills": [
24
+ {
25
+ "id": "anfrage",
26
+ "name": "Anfrage hinterlassen",
27
+ "description": "Eine Anfrage anlegen. Ein Mensch im Betrieb meldet sich zurück. Keine Zusage und kein Preis durch den Assistenten.",
28
+ "tags": ["anfrage", "kontakt"]
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """Validate an A2A Agent Card, an MCP Server Card or an AI catalog (offline, stdlib only).
3
+
4
+ Usage:
5
+ agent-card validate path/to/agent-card.json (or: python3 tools/validate_agent_card.py ...)
6
+ python3 tools/validate_agent_card.py card.json --type server-card --strict
7
+
8
+ Exit code 0 = valid (warnings allowed), 1 = errors found (or warnings with --strict).
9
+
10
+ This is a pragmatic subset check of the required fields, not a full JSON Schema
11
+ validation. Standards move: check the official specs before you rely on it.
12
+ Nützlich zum schnellen Prüfen der Pflichtfelder, ersetzt keine Volltest-Validierung.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+ import sys
20
+ from typing import Any, List, Tuple
21
+
22
+ KNOWN_BINDINGS = {"JSONRPC", "GRPC", "HTTP+JSON", "MCP"}
23
+ SEMVER = re.compile(r"^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$")
24
+ MEDIA_TYPE = re.compile(r"^[A-Za-z0-9.+-]+/[A-Za-z0-9.+-]+$")
25
+ EURO = re.compile(r"(€|\bEUR\b|\bEuro\b)", re.IGNORECASE)
26
+ PHONE = re.compile(r"(\+\d{2}[\d\s/()-]{6,}|\b0\d{2,5}[\s/-]?\d{4,})")
27
+ EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
28
+ CATALOG_TYPES = {"application/a2a-agent-card+json", "application/mcp-server-card+json"}
29
+
30
+
31
+ def strip_comments(text: str) -> str:
32
+ """Remove // line comments and /* block */ comments outside of strings (JSONC)."""
33
+ out: List[str] = []
34
+ i, n = 0, len(text)
35
+ in_str = False
36
+ while i < n:
37
+ c = text[i]
38
+ if in_str:
39
+ out.append(c)
40
+ if c == "\\" and i + 1 < n:
41
+ out.append(text[i + 1])
42
+ i += 1
43
+ elif c == '"':
44
+ in_str = False
45
+ elif c == '"':
46
+ in_str = True
47
+ out.append(c)
48
+ elif text.startswith("//", i):
49
+ while i < n and text[i] != "\n":
50
+ i += 1
51
+ continue
52
+ elif text.startswith("/*", i):
53
+ end = text.find("*/", i + 2)
54
+ i = n if end == -1 else end + 2
55
+ continue
56
+ else:
57
+ out.append(c)
58
+ i += 1
59
+ return "".join(out)
60
+
61
+
62
+ def load(path: str) -> Any:
63
+ with open(path, encoding="utf-8") as fh:
64
+ return json.loads(strip_comments(fh.read()))
65
+
66
+
67
+ def _is_str(v: Any) -> bool:
68
+ return isinstance(v, str) and v.strip() != ""
69
+
70
+
71
+ def _https(v: Any) -> bool:
72
+ return _is_str(v) and v.startswith("https://") and " " not in v
73
+
74
+
75
+ def detect_type(doc: Any) -> str:
76
+ if isinstance(doc, dict):
77
+ if "entries" in doc:
78
+ return "catalog"
79
+ if "remotes" in doc or "$schema" in doc:
80
+ return "server-card"
81
+ return "agent-card"
82
+
83
+
84
+ def _texts(doc: Any):
85
+ if isinstance(doc, dict):
86
+ for k, v in doc.items():
87
+ if k in ("url", "$schema", "websiteUrl", "documentationUrl", "identifier"):
88
+ continue
89
+ yield from _texts(v)
90
+ elif isinstance(doc, list):
91
+ for v in doc:
92
+ yield from _texts(v)
93
+ elif isinstance(doc, str):
94
+ yield doc
95
+
96
+
97
+ def content_warnings(doc: Any) -> List[str]:
98
+ w: List[str] = []
99
+ for t in _texts(doc):
100
+ if EURO.search(t):
101
+ w.append("price/currency in card text (%r): prices belong to a human offer" % t[:40])
102
+ if PHONE.search(t):
103
+ w.append("phone number in card text (%r)" % t[:40])
104
+ if EMAIL.search(t):
105
+ w.append("e-mail address in card text (%r)" % t[:40])
106
+ return w
107
+
108
+
109
+ def validate_agent_card(d: Any) -> Tuple[List[str], List[str]]:
110
+ e: List[str] = []
111
+ w: List[str] = []
112
+ if not isinstance(d, dict):
113
+ return ["top level must be a JSON object"], w
114
+ for k in ("name", "description", "version"):
115
+ if not _is_str(d.get(k)):
116
+ e.append("%s: required, non-empty string" % k)
117
+ if _is_str(d.get("version")) and not SEMVER.match(d["version"]):
118
+ w.append("version %r is not semver (x.y.z)" % d["version"])
119
+ ifaces = d.get("supportedInterfaces")
120
+ if not isinstance(ifaces, list) or not ifaces:
121
+ e.append("supportedInterfaces: required, at least one entry")
122
+ else:
123
+ for i, it in enumerate(ifaces):
124
+ p = "supportedInterfaces[%d]" % i
125
+ if not isinstance(it, dict):
126
+ e.append(p + ": must be an object")
127
+ continue
128
+ if not _https(it.get("url")):
129
+ e.append(p + ".url: required, https URL")
130
+ if not _is_str(it.get("protocolBinding")):
131
+ e.append(p + ".protocolBinding: required")
132
+ elif it["protocolBinding"] not in KNOWN_BINDINGS:
133
+ w.append("%s.protocolBinding %r not in %s" % (p, it["protocolBinding"], sorted(KNOWN_BINDINGS)))
134
+ if not _is_str(it.get("protocolVersion")):
135
+ e.append(p + ".protocolVersion: required")
136
+ prov = d.get("provider")
137
+ if not isinstance(prov, dict):
138
+ w.append("provider: recommended (organization, url)")
139
+ else:
140
+ if not _is_str(prov.get("organization")):
141
+ e.append("provider.organization: required when provider is given")
142
+ if not _https(prov.get("url")):
143
+ e.append("provider.url: required when provider is given, https URL")
144
+ if "documentationUrl" in d and not _https(d["documentationUrl"]):
145
+ e.append("documentationUrl: must be an https URL")
146
+ if not isinstance(d.get("capabilities"), dict):
147
+ e.append("capabilities: required object (may be empty)")
148
+ for k in ("defaultInputModes", "defaultOutputModes"):
149
+ v = d.get(k)
150
+ if not isinstance(v, list) or not v:
151
+ e.append("%s: required, non-empty list of media types" % k)
152
+ else:
153
+ for m in v:
154
+ if not (isinstance(m, str) and MEDIA_TYPE.match(m)):
155
+ e.append("%s: %r is not a media type" % (k, m))
156
+ skills = d.get("skills")
157
+ if not isinstance(skills, list) or not skills:
158
+ e.append("skills: required, at least one skill")
159
+ else:
160
+ seen = set()
161
+ for i, s in enumerate(skills):
162
+ p = "skills[%d]" % i
163
+ if not isinstance(s, dict):
164
+ e.append(p + ": must be an object")
165
+ continue
166
+ for k in ("id", "name", "description"):
167
+ if not _is_str(s.get(k)):
168
+ e.append("%s.%s: required, non-empty string" % (p, k))
169
+ tags = s.get("tags")
170
+ if not isinstance(tags, list) or not tags or not all(_is_str(t) for t in tags):
171
+ e.append(p + ".tags: required, non-empty list of strings")
172
+ if _is_str(s.get("id")):
173
+ if s["id"] in seen:
174
+ e.append("%s.id %r is duplicated" % (p, s["id"]))
175
+ seen.add(s["id"])
176
+ w.extend(content_warnings(d))
177
+ return e, w
178
+
179
+
180
+ def validate_server_card(d: Any) -> Tuple[List[str], List[str]]:
181
+ e: List[str] = []
182
+ w: List[str] = []
183
+ if not isinstance(d, dict):
184
+ return ["top level must be a JSON object"], w
185
+ for k in ("$schema", "name", "version", "description"):
186
+ if not _is_str(d.get(k)):
187
+ e.append("%s: required, non-empty string" % k)
188
+ if _is_str(d.get("version")) and not SEMVER.match(d["version"]):
189
+ w.append("version %r is not semver (x.y.z)" % d["version"])
190
+ if "websiteUrl" in d and not _https(d["websiteUrl"]):
191
+ e.append("websiteUrl: must be an https URL")
192
+ remotes = d.get("remotes")
193
+ if remotes is None:
194
+ w.append("remotes: none given, assistants cannot connect")
195
+ elif not isinstance(remotes, list) or not remotes:
196
+ e.append("remotes: must be a non-empty list")
197
+ else:
198
+ for i, r in enumerate(remotes):
199
+ p = "remotes[%d]" % i
200
+ if not isinstance(r, dict):
201
+ e.append(p + ": must be an object")
202
+ continue
203
+ if r.get("type") not in ("streamable-http", "sse"):
204
+ e.append(p + ".type: must be 'streamable-http' or 'sse'")
205
+ if not _https(r.get("url")):
206
+ e.append(p + ".url: required, https URL")
207
+ w.extend(content_warnings(d))
208
+ return e, w
209
+
210
+
211
+ def validate_catalog(d: Any) -> Tuple[List[str], List[str]]:
212
+ e: List[str] = []
213
+ w: List[str] = []
214
+ if not isinstance(d, dict):
215
+ return ["top level must be a JSON object"], w
216
+ if not _is_str(d.get("specVersion")):
217
+ e.append("specVersion: required")
218
+ entries = d.get("entries")
219
+ if not isinstance(entries, list) or not entries:
220
+ e.append("entries: required, at least one entry")
221
+ return e, w
222
+ ids = set()
223
+ for i, it in enumerate(entries):
224
+ p = "entries[%d]" % i
225
+ if not isinstance(it, dict):
226
+ e.append(p + ": must be an object")
227
+ continue
228
+ if not _is_str(it.get("identifier")):
229
+ e.append(p + ".identifier: required")
230
+ elif it["identifier"] in ids:
231
+ e.append("%s.identifier %r is duplicated" % (p, it["identifier"]))
232
+ else:
233
+ ids.add(it["identifier"])
234
+ if it.get("type") not in CATALOG_TYPES:
235
+ w.append("%s.type %r is not one of %s" % (p, it.get("type"), sorted(CATALOG_TYPES)))
236
+ if not _https(it.get("url")):
237
+ e.append(p + ".url: required, https URL")
238
+ return e, w
239
+
240
+
241
+ def validate(doc: Any, kind: str = "auto") -> Tuple[List[str], List[str]]:
242
+ if kind == "auto":
243
+ kind = detect_type(doc)
244
+ return {"agent-card": validate_agent_card, "server-card": validate_server_card,
245
+ "catalog": validate_catalog}[kind](doc)
246
+
247
+
248
+ def main(argv: List[str] = None) -> int:
249
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
250
+ ap.add_argument("file")
251
+ ap.add_argument("--type", choices=["auto", "agent-card", "server-card", "catalog"], default="auto")
252
+ ap.add_argument("--strict", action="store_true", help="treat warnings as errors")
253
+ a = ap.parse_args(argv)
254
+ try:
255
+ doc = load(a.file)
256
+ except (OSError, ValueError) as ex:
257
+ print("ERROR cannot read %s: %s" % (a.file, ex))
258
+ return 1
259
+ errors, warnings = validate(doc, a.type)
260
+ for m in errors:
261
+ print("ERROR " + m)
262
+ for m in warnings:
263
+ print("WARN " + m)
264
+ if errors or (a.strict and warnings):
265
+ print("INVALID: %d error(s), %d warning(s)" % (len(errors), len(warnings)))
266
+ return 1
267
+ print("OK: %s (%d warning(s))" % (a.file, len(warnings)))
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ sys.exit(main())
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-card-starter
3
+ Version: 0.2.0
4
+ Summary: Make a small business readable for AI assistants: write and check an A2A agent card or MCP server card. Standard library only.
5
+ Author-email: flowboxx <kontakt@flowboxx.de>
6
+ License: MIT
7
+ Project-URL: Homepage, https://flowboxx.de/
8
+ Project-URL: Source, https://github.com/flowboxx/agent-card-starter
9
+ Project-URL: Issues, https://github.com/flowboxx/agent-card-starter/issues
10
+ Keywords: a2a,agent-card,mcp,mcp-server-card,well-known,small-business
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # agent-card-starter
21
+
22
+ [![tests](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml)
23
+ ![license MIT](https://img.shields.io/badge/license-MIT-blue)
24
+ ![python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
25
+
26
+ **In 60 seconds:** AI assistants read a small JSON file to learn what your business does. This tool writes that file for you and checks it. Offline, no account, no dependencies.
27
+
28
+ ```bash
29
+ pip install git+https://github.com/flowboxx/agent-card-starter
30
+ agent-card init --name "Muster Roofing Ltd" --description "Roofer in Musterstadt, repairs and new roofs." --website https://www.example.com --skill "Flat roof sealing"
31
+ agent-card validate .well-known/agent-card.json --strict
32
+ ```
33
+
34
+ Upload the file to `https://<your-domain>/.well-known/agent-card.json` and you are readable. Nothing here guarantees that an assistant will mention you.
35
+
36
+ **In 60 Sekunden:** KI-Assistenten lesen eine kleine Datei, um zu erfahren, was Ihr Betrieb macht. Dieses Werkzeug schreibt die Datei und prüft sie. Ohne Netz, ohne Konto, ohne Fremdpakete. Befehle siehe oben, die Datei kommt nach `https://<Ihre-Domain>/.well-known/agent-card.json`.
37
+
38
+ <!-- Picture idea: a 20 second terminal recording (init, then validate with OK) of the three commands above. Fictional business only. -->
39
+
40
+ Starter kit to make a small business readable and reachable for AI assistants:
41
+ an A2A agent card, an MCP server card, a minimal MCP example server and a validator.
42
+ Standard library only, Python 3.9+, MIT licensed.
43
+
44
+ Starter-Set, damit ein kleiner Betrieb für KI-Assistenten lesbar und erreichbar wird:
45
+ Agentenkarte (A2A), Server-Karte (MCP), ein minimaler MCP-Beispielserver und ein Prüfskript.
46
+ Nur Python-Standardbibliothek, ab Python 3.9, MIT-Lizenz.
47
+
48
+ Maintained by [flowboxx](https://flowboxx.de/?ref=github), an agency that builds AI doors for businesses.
49
+ Gepflegt von [flowboxx](https://flowboxx.de/?ref=github), einer Agentur, die KI-Türen für Betriebe baut.
50
+
51
+ ---
52
+
53
+ ## English
54
+
55
+ ### What is in here
56
+
57
+ | Path | What it is |
58
+ | --- | --- |
59
+ | `examples/well-known/agent-card.json` | A2A agent card for a fictional "Beispiel Dachdecker GmbH" |
60
+ | `examples/well-known/agent.json` | Byte-identical copy for older readers |
61
+ | `examples/agent-card.annotated.jsonc` | Same card with comments (documentation only) |
62
+ | `examples/mcp-server-card.json` | MCP server card for the same business |
63
+ | `templates/` | Blank-ish templates: server card and AI catalog |
64
+ | `server/mcp_server.py` | MCP example server over stdio: tools `leistungen` and `anfrage_anlegen` (stub) |
65
+ | `agent_card/` | The `agent-card` command: `init` writes a first card, `validate` checks card, server card and catalog (offline) |
66
+ | `tools/validate_agent_card.py` | Same validator as a plain script |
67
+ | `tests/` | pytest suite |
68
+
69
+ ### Quick start
70
+
71
+ ```bash
72
+ agent-card init --name "Your Business" --description "What you do and where." --website https://www.example.com
73
+ agent-card validate .well-known/agent-card.json --strict
74
+ # from a clone, without installing:
75
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
76
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
77
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
78
+ python3 -m pip install pytest && python3 -m pytest -q
79
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
80
+ ```
81
+
82
+ ### Use it for your business
83
+
84
+ 1. Copy `examples/well-known/agent-card.json`, replace name, description, provider, skills.
85
+ 2. List only services you really offer. One skill per service, stable ids.
86
+ 3. Keep prices, phone numbers and promises out of the card. A human answers those.
87
+ 4. Run the validator, publish the file at `https://<your-domain>/.well-known/agent-card.json`.
88
+ 5. Point `supportedInterfaces` at an endpoint you really operate. The example server
89
+ speaks MCP over stdio for learning. A public endpoint needs HTTP transport,
90
+ rate limiting, spam protection and a privacy notice. `anfrage_anlegen` is a stub on purpose.
91
+
92
+ ### Notes on the standards
93
+
94
+ - The agent card follows the A2A v1.0 field names (`supportedInterfaces`, `skills`, `provider`).
95
+ - The server card follows the MCP Server Card proposal (SEP-2127). It is an extension and
96
+ the schema may still change. Check the current schema before you publish.
97
+ - The validator checks required fields and a few sanity rules. It is not a full JSON Schema validation.
98
+ - Nothing here guarantees that an assistant will mention or recommend you.
99
+
100
+ ---
101
+
102
+ ## Deutsch
103
+
104
+ ### Was drin ist
105
+
106
+ | Pfad | Inhalt |
107
+ | --- | --- |
108
+ | `examples/well-known/agent-card.json` | A2A-Agentenkarte für die fiktive "Beispiel Dachdecker GmbH" |
109
+ | `examples/well-known/agent.json` | Byte-gleiche Kopie für ältere Leser |
110
+ | `examples/agent-card.annotated.jsonc` | Dieselbe Karte mit Kommentaren (nur zur Erklärung) |
111
+ | `examples/mcp-server-card.json` | MCP-Server-Karte für denselben Betrieb |
112
+ | `templates/` | Vorlagen: Server-Karte und KI-Katalog |
113
+ | `server/mcp_server.py` | MCP-Beispielserver über stdio: Werkzeuge `leistungen` und `anfrage_anlegen` (Stub) |
114
+ | `agent_card/` | Der Befehl `agent-card`: `init` schreibt eine erste Karte, `validate` prüft Karte, Server-Karte und Katalog (ohne Netz) |
115
+ | `tools/validate_agent_card.py` | Dasselbe Prüfskript als einfache Datei |
116
+ | `tests/` | pytest-Tests |
117
+
118
+ ### Schnellstart
119
+
120
+ ```bash
121
+ agent-card init --name "Ihr Betrieb" --description "Was Sie tun und wo." --website https://www.example.com
122
+ agent-card validate .well-known/agent-card.json --strict
123
+ # aus einem Klon, ohne Installation:
124
+ python3 tools/validate_agent_card.py examples/well-known/agent-card.json
125
+ python3 tools/validate_agent_card.py examples/mcp-server-card.json --strict
126
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | python3 server/mcp_server.py
127
+ python3 -m pip install pytest && python3 -m pytest -q
128
+ docker build -t agent-card-starter . && docker run -i --rm agent-card-starter # MCP over stdio
129
+ ```
130
+
131
+ ### Für den eigenen Betrieb nutzen
132
+
133
+ 1. `examples/well-known/agent-card.json` kopieren, Name, Beschreibung, Anbieter und Fähigkeiten ersetzen.
134
+ 2. Nur Leistungen eintragen, die Sie wirklich anbieten. Eine Fähigkeit je Leistung, stabile Ids.
135
+ 3. Keine Preise, Telefonnummern oder Zusagen in die Karte. Das beantwortet ein Mensch.
136
+ 4. Prüfskript laufen lassen, Datei unter `https://<Ihre-Domain>/.well-known/agent-card.json` ablegen.
137
+ 5. `supportedInterfaces` auf einen Endpunkt zeigen lassen, den Sie wirklich betreiben. Der Beispielserver
138
+ spricht MCP über stdio zum Lernen. Ein öffentlicher Endpunkt braucht HTTP, Mengenbegrenzung,
139
+ Spam-Schutz und einen Datenschutzhinweis. `anfrage_anlegen` ist absichtlich ein Stub und speichert nichts.
140
+
141
+ ### Hinweise zu den Standards
142
+
143
+ - Die Agentenkarte folgt den Feldnamen von A2A v1.0 (`supportedInterfaces`, `skills`, `provider`).
144
+ - Die Server-Karte folgt dem MCP-Vorschlag Server Card (SEP-2127). Das ist eine Erweiterung,
145
+ das Schema kann sich noch ändern. Vor dem Veröffentlichen das aktuelle Schema prüfen.
146
+ - Das Prüfskript prüft Pflichtfelder und einige Plausibilitäten, keine vollständige JSON-Schema-Validierung.
147
+ - Nichts hier garantiert, dass ein Assistent Sie nennt oder empfiehlt.
148
+
149
+ ---
150
+
151
+ ## Built with this? Tell us / Damit gebaut? Erzählen Sie es uns
152
+
153
+ If you published a card with this kit, show it in [Discussions, Show and tell](../../discussions/categories/show-and-tell). We read every one. If you like, add the badge to your README:
154
+
155
+ Wenn Sie mit diesem Kit eine Karte veröffentlicht haben, zeigen Sie sie unter [Discussions, Show and tell](../../discussions/categories/show-and-tell). Das Abzeichen für Ihre README:
156
+
157
+ ```markdown
158
+ [![built with agent-card-starter](https://img.shields.io/badge/built%20with-agent--card--starter-2b6cb0)](https://github.com/flowboxx/agent-card-starter)
159
+ ```
160
+
161
+ ---
162
+
163
+ ## As is / Ohne Gewähr
164
+
165
+ This is a starting point, provided "as is" under the MIT license, without warranty of any kind. Standards around agent cards and server cards are young and change. Check the current specifications before you publish. Nothing here is legal advice, and nothing guarantees that an assistant will mention or recommend your business. Changes are listed in [CHANGELOG.md](CHANGELOG.md), how to help is in [CONTRIBUTING.md](CONTRIBUTING.md).
166
+
167
+ Dies ist ein Ausgangspunkt, bereitgestellt "wie besehen" unter der MIT-Lizenz, ohne Gewähr. Die Standards rund um Agentenkarten und Server-Karten sind jung und ändern sich. Prüfen Sie vor dem Veröffentlichen die aktuellen Spezifikationen. Nichts hier ist Rechtsberatung, und nichts garantiert, dass ein Assistent Ihren Betrieb nennt oder empfiehlt. Änderungen stehen in [CHANGELOG.md](CHANGELOG.md), Mitmachen steht in [CONTRIBUTING.md](CONTRIBUTING.md).
168
+
169
+ MIT License, see `LICENSE`. Questions and contributions welcome via issues. Fragen und Beiträge gern als Issue.
170
+
171
+ Built and maintained by [flowboxx](https://flowboxx.de/?ref=github), Germany.
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ agent_card/__init__.py
5
+ agent_card/__main__.py
6
+ agent_card/cli.py
7
+ agent_card/validator.py
8
+ agent_card/data/agent-card.template.json
9
+ agent_card_starter.egg-info/PKG-INFO
10
+ agent_card_starter.egg-info/SOURCES.txt
11
+ agent_card_starter.egg-info/dependency_links.txt
12
+ agent_card_starter.egg-info/entry_points.txt
13
+ agent_card_starter.egg-info/top_level.txt
14
+ tests/test_cli.py
15
+ tests/test_hygiene.py
16
+ tests/test_server.py
17
+ tests/test_validator.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-card = agent_card.cli:main
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agent-card-starter"
7
+ version = "0.2.0"
8
+ description = "Make a small business readable for AI assistants: write and check an A2A agent card or MCP server card. Standard library only."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "flowboxx", email = "kontakt@flowboxx.de" }]
13
+ keywords = ["a2a", "agent-card", "mcp", "mcp-server-card", "well-known", "small-business"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Internet :: WWW/HTTP",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.urls]
23
+ Homepage = "https://flowboxx.de/"
24
+ Source = "https://github.com/flowboxx/agent-card-starter"
25
+ Issues = "https://github.com/flowboxx/agent-card-starter/issues"
26
+
27
+ [project.scripts]
28
+ agent-card = "agent_card.cli:main"
29
+
30
+ [tool.setuptools]
31
+ packages = ["agent_card"]
32
+
33
+ [tool.setuptools.package-data]
34
+ agent_card = ["data/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,51 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+ from agent_card import cli, validator
7
+
8
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
+
10
+
11
+ def test_init_writes_a_valid_card_without_warnings(tmp_path, capsys):
12
+ rc = cli.main(["init", "--name", 'Muster "Dach" GmbH', "--description", "Dachdecker in Musterstadt.", "--website", "https://www.beispiel.example/", "--out", str(tmp_path)])
13
+ assert rc == 0
14
+ p = tmp_path / ".well-known" / "agent-card.json"
15
+ card = json.loads(p.read_text(encoding="utf-8"))
16
+ assert card["name"] == 'Muster "Dach" GmbH' and card["provider"]["url"] == "https://www.beispiel.example"
17
+ assert card["supportedInterfaces"][0]["url"] == "https://www.beispiel.example/mcp"
18
+ assert validator.validate(card, "agent-card") == ([], [])
19
+ assert "Wrote" in capsys.readouterr().out
20
+
21
+
22
+ def test_init_refuses_to_overwrite_and_rejects_bad_url(tmp_path):
23
+ args = ["init", "--name", "A", "--description", "B", "--website", "https://x.example", "--out", str(tmp_path)]
24
+ assert cli.main(args) == 0
25
+ assert cli.main(args) == 1
26
+ assert cli.main(args + ["--force"]) == 0
27
+ bad = ["init", "--name", "A", "--description", "B", "--website", "http://x.example", "--out", str(tmp_path / "n")]
28
+ assert cli.main(bad) == 1 and not (tmp_path / "n").exists()
29
+
30
+
31
+ def test_validate_subcommand_and_old_script_agree():
32
+ card = os.path.join(ROOT, "examples", "well-known", "agent-card.json")
33
+ assert cli.main(["validate", card]) == 0
34
+ r = subprocess.run([sys.executable, os.path.join(ROOT, "tools", "validate_agent_card.py"), card], capture_output=True, text=True)
35
+ assert r.returncode == 0 and r.stdout.startswith("OK")
36
+ r = subprocess.run([sys.executable, "-m", "agent_card", "validate", card, "--strict"], capture_output=True, text=True, cwd=ROOT)
37
+ assert r.returncode == 0
38
+
39
+
40
+ def test_package_carries_its_template():
41
+ assert "{name}" in cli._template()
42
+
43
+
44
+ def test_init_skills_become_stable_ids_and_keep_the_inquiry_skill_last(tmp_path):
45
+ args = ["init", "--name", "A", "--description", "B", "--website", "https://x.example", "--out", str(tmp_path),
46
+ "--skill", "Flachdach abdichten", "--skill", "Dämmung: Dach und Keller dämmen.", "--skill", "Flachdach abdichten"]
47
+ assert cli.main(args) == 0
48
+ card = json.loads((tmp_path / ".well-known" / "agent-card.json").read_text(encoding="utf-8"))
49
+ ids = [s["id"] for s in card["skills"]]
50
+ assert ids == ["flachdach-abdichten", "daemmung", "flachdach-abdichten-2", "anfrage"]
51
+ assert card["skills"][1]["description"] == "Dach und Keller dämmen." and validator.validate(card, "agent-card") == ([], [])
@@ -0,0 +1,32 @@
1
+ """Public repo hygiene: no internal names, paths, servers, no hint on how we build."""
2
+ import os
3
+ import re
4
+
5
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
6
+ FORBIDDEN = [r"/Users/", r"/opt/", r"\bserverb\b", r"\bservera\b", r"\.claude", r"memory/",
7
+ r"\bDPI\b", r"Tafelpr", r"Anthropic", r"ChatGPT", r"\bClaude\b", r"\bKimi\b",
8
+ r"mit KI (gebaut|erstellt|geschrieben)", r"KI-generiert", r"cockpit", r"Sentinal",
9
+ r"Frieda", r"Enrico", "—", "–"]
10
+
11
+
12
+ def files():
13
+ for d, dirs, fs in os.walk(ROOT):
14
+ dirs[:] = [x for x in dirs if x not in (".git", "__pycache__", ".pytest_cache")]
15
+ for f in fs:
16
+ if f == "test_hygiene.py" or f.endswith(".pyc") or f == "glama.json": # glama.json must name the maintainer account (GitHub handle)
17
+ continue
18
+ yield os.path.join(d, f)
19
+
20
+
21
+ def test_no_forbidden_terms_in_any_file():
22
+ hits = []
23
+ for p in files():
24
+ txt = open(p, encoding="utf-8").read()
25
+ for pat in FORBIDDEN:
26
+ if re.search(pat, txt):
27
+ hits.append((os.path.relpath(p, ROOT), pat))
28
+ assert hits == []
29
+
30
+
31
+ def test_license_is_mit():
32
+ assert "MIT License" in open(os.path.join(ROOT, "LICENSE")).read()
@@ -0,0 +1,73 @@
1
+ import io
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+ import mcp_server as s
8
+
9
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+
11
+
12
+ def call(method, params=None, rid=1):
13
+ return s.handle({"jsonrpc": "2.0", "id": rid, "method": method, "params": params or {}})
14
+
15
+
16
+ def payload(res):
17
+ return json.loads(res["result"]["content"][0]["text"])
18
+
19
+
20
+ def test_initialize():
21
+ r = call("initialize")
22
+ assert r["result"]["protocolVersion"] == s.PROTOCOL_VERSION
23
+ assert "tools" in r["result"]["capabilities"]
24
+
25
+
26
+ def test_notification_has_no_reply():
27
+ assert s.handle({"jsonrpc": "2.0", "method": "notifications/initialized"}) is None
28
+
29
+
30
+ def test_tools_list_names():
31
+ names = [t["name"] for t in call("tools/list")["result"]["tools"]]
32
+ assert names == ["leistungen", "anfrage_anlegen"]
33
+
34
+
35
+ def test_leistungen():
36
+ p = payload(call("tools/call", {"name": "leistungen", "arguments": {}}))
37
+ assert p["leistungen"] and "Mensch" in p["hinweis"]
38
+
39
+
40
+ def test_anfrage_is_stub_and_validates():
41
+ ok = payload(call("tools/call", {"name": "anfrage_anlegen", "arguments": {
42
+ "leistung": "flachdach", "nachricht": "Dach undicht", "rueckmeldung": "a@b.example"}}))
43
+ assert ok["stub"] is True and ok["angelegt"] is False and ok["referenz"].startswith("demo-")
44
+ bad = call("tools/call", {"name": "anfrage_anlegen", "arguments": {"leistung": "nix", "nachricht": ""}})
45
+ assert bad["result"]["isError"] is True
46
+ assert len(payload(bad)["fehler"]) == 3
47
+
48
+
49
+ def test_unknown_tool_and_method_and_invalid_request():
50
+ assert call("tools/call", {"name": "nope"})["error"]["code"] == -32602
51
+ assert call("nope")["error"]["code"] == -32601
52
+ assert s.handle({"hello": 1})["error"]["code"] == -32600
53
+
54
+
55
+ def test_serve_handles_garbage_lines():
56
+ out = io.StringIO()
57
+ inp = io.StringIO('not json\n\n{"jsonrpc":"2.0","id":7,"method":"ping"}\n')
58
+ s.serve(inp, out)
59
+ lines = [json.loads(x) for x in out.getvalue().splitlines()]
60
+ assert lines[0]["error"]["code"] == -32700 and lines[1]["id"] == 7
61
+
62
+
63
+ def test_stdio_roundtrip():
64
+ req = '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n'
65
+ r = subprocess.run([sys.executable, os.path.join(ROOT, "server", "mcp_server.py")],
66
+ input=req, capture_output=True, text=True, timeout=20)
67
+ assert json.loads(r.stdout)["result"]["tools"]
68
+
69
+
70
+ def test_card_matches_server_tools_story():
71
+ # the example card promises an 'anfrage' skill, the server offers the tool
72
+ card = json.load(open(os.path.join(ROOT, "examples", "well-known", "agent-card.json"), encoding="utf-8"))
73
+ assert "anfrage" in [x["id"] for x in card["skills"]]
@@ -0,0 +1,116 @@
1
+ import copy
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+ import pytest
8
+
9
+ import validate_agent_card as v
10
+
11
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
+ CARD = os.path.join(ROOT, "examples", "well-known", "agent-card.json")
13
+
14
+
15
+ def card():
16
+ return v.load(CARD)
17
+
18
+
19
+ def test_example_card_is_valid_without_warnings():
20
+ e, w = v.validate(card())
21
+ assert e == [] and w == []
22
+
23
+
24
+ def test_agent_json_copy_is_identical():
25
+ a = open(CARD, "rb").read()
26
+ b = open(os.path.join(ROOT, "examples", "well-known", "agent.json"), "rb").read()
27
+ assert a == b
28
+
29
+
30
+ def test_annotated_file_equals_plain_json():
31
+ annotated = v.load(os.path.join(ROOT, "examples", "agent-card.annotated.jsonc"))
32
+ assert annotated == card()
33
+
34
+
35
+ def test_server_card_and_catalog_templates_are_valid():
36
+ for f in ("templates/mcp-server-card.template.json", "examples/mcp-server-card.json",
37
+ "templates/ai-catalog.template.json"):
38
+ e, w = v.validate(v.load(os.path.join(ROOT, f)))
39
+ assert e == [] and w == [], f
40
+
41
+
42
+ @pytest.mark.parametrize("field", ["name", "description", "version", "supportedInterfaces",
43
+ "capabilities", "defaultInputModes", "defaultOutputModes", "skills"])
44
+ def test_missing_required_field_is_error(field):
45
+ c = card()
46
+ del c[field]
47
+ e, _ = v.validate(c)
48
+ assert any(x.startswith(field) for x in e)
49
+
50
+
51
+ def test_http_url_rejected():
52
+ c = card()
53
+ c["supportedInterfaces"][0]["url"] = "http://tuer.example/mcp"
54
+ e, _ = v.validate(c)
55
+ assert any("https" in x for x in e)
56
+
57
+
58
+ def test_skill_without_tags_and_duplicate_id():
59
+ c = card()
60
+ c["skills"][0].pop("tags")
61
+ c["skills"][1]["id"] = c["skills"][2]["id"]
62
+ e, _ = v.validate(c)
63
+ assert any("tags" in x for x in e) and any("duplicated" in x for x in e)
64
+
65
+
66
+ def test_unknown_binding_and_bad_version_are_warnings_only():
67
+ c = card()
68
+ c["supportedInterfaces"][0]["protocolBinding"] = "CARRIERPIGEON"
69
+ c["version"] = "one"
70
+ e, w = v.validate(c)
71
+ assert e == [] and len(w) == 2
72
+
73
+
74
+ @pytest.mark.parametrize("text", ["ab 99 Euro pro Stunde", "Preis 50 €", "Tel +49 2361 123456", "info@firma.example"])
75
+ def test_price_phone_mail_in_text_warn(text):
76
+ c = copy.deepcopy(card())
77
+ c["skills"][0]["description"] = text
78
+ e, w = v.validate(c)
79
+ assert e == [] and w
80
+
81
+
82
+ def test_non_object_is_error():
83
+ e, _ = v.validate([], "agent-card")
84
+ assert e
85
+
86
+
87
+ def test_comment_stripper_keeps_urls_in_strings():
88
+ txt = '{"u": "https://x.example/a//b", /* c */ "k": 1} // end'
89
+ assert json.loads(v.strip_comments(txt)) == {"u": "https://x.example/a//b", "k": 1}
90
+
91
+
92
+ def test_server_card_needs_schema_and_remote_type():
93
+ d = v.load(os.path.join(ROOT, "examples", "mcp-server-card.json"))
94
+ d.pop("$schema")
95
+ d["remotes"][0]["type"] = "carrier"
96
+ e, _ = v.validate(d)
97
+ assert any("$schema" in x for x in e) and any("type" in x for x in e)
98
+
99
+
100
+ def test_catalog_duplicate_identifier():
101
+ d = v.load(os.path.join(ROOT, "templates", "ai-catalog.template.json"))
102
+ d["entries"][1]["identifier"] = d["entries"][0]["identifier"]
103
+ e, _ = v.validate(d)
104
+ assert any("duplicated" in x for x in e)
105
+
106
+
107
+ def test_cli_exit_codes(tmp_path):
108
+ script = os.path.join(ROOT, "tools", "validate_agent_card.py")
109
+ ok = subprocess.run([sys.executable, script, CARD], capture_output=True, text=True)
110
+ assert ok.returncode == 0 and "OK" in ok.stdout
111
+ bad = tmp_path / "bad.json"
112
+ bad.write_text('{"name": "x"}')
113
+ r = subprocess.run([sys.executable, script, str(bad)], capture_output=True, text=True)
114
+ assert r.returncode == 1 and "INVALID" in r.stdout
115
+ r = subprocess.run([sys.executable, script, str(tmp_path / "missing.json")], capture_output=True, text=True)
116
+ assert r.returncode == 1