agent-card-starter 0.2.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.
- agent_card/__init__.py +2 -0
- agent_card/__main__.py +5 -0
- agent_card/cli.py +105 -0
- agent_card/data/agent-card.template.json +31 -0
- agent_card/validator.py +272 -0
- agent_card_starter-0.2.0.dist-info/METADATA +171 -0
- agent_card_starter-0.2.0.dist-info/RECORD +11 -0
- agent_card_starter-0.2.0.dist-info/WHEEL +5 -0
- agent_card_starter-0.2.0.dist-info/entry_points.txt +2 -0
- agent_card_starter-0.2.0.dist-info/licenses/LICENSE +21 -0
- agent_card_starter-0.2.0.dist-info/top_level.txt +1 -0
agent_card/__init__.py
ADDED
agent_card/__main__.py
ADDED
agent_card/cli.py
ADDED
|
@@ -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
|
+
}
|
agent_card/validator.py
ADDED
|
@@ -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
|
+
[](../../actions/workflows/ci.yml)
|
|
23
|
+

|
|
24
|
+

|
|
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
|
+
[](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,11 @@
|
|
|
1
|
+
agent_card/__init__.py,sha256=ORgqAZYtC0VgENTUF4RUU3tQYnzKNCmLdc1Yg4RhFAw,121
|
|
2
|
+
agent_card/__main__.py,sha256=luiLj9IADr6Ex8TNsHdq3fmYeFmmxWGwgmFMGOyl2GE,62
|
|
3
|
+
agent_card/cli.py,sha256=JTk0klqEpDaNc9dG3sFAUTr340wqub_0E5DUOvdx4_g,4654
|
|
4
|
+
agent_card/validator.py,sha256=3Yf6Fq59GXaxkIDc8MDsc9dkEuyGX88Z8GEdrHf_oc0,10356
|
|
5
|
+
agent_card/data/agent-card.template.json,sha256=ck1-mmmWLFlbuQZIr66EacYiiKEaCyt8v7t7FZihsPE,769
|
|
6
|
+
agent_card_starter-0.2.0.dist-info/licenses/LICENSE,sha256=va0C7WxTZdn3cANATOM4RIwhYveIH0Unpv_aipbV2Ks,1065
|
|
7
|
+
agent_card_starter-0.2.0.dist-info/METADATA,sha256=agnyX4WbawiNEX6b0bK-6XeWGo_rgtq2ZabFfbro-lo,9765
|
|
8
|
+
agent_card_starter-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
agent_card_starter-0.2.0.dist-info/entry_points.txt,sha256=JIGFWzWnUt83no3m7YcJVX5M03lH7-6gNL9HTqzv-ck,51
|
|
10
|
+
agent_card_starter-0.2.0.dist-info/top_level.txt,sha256=pcy9JqHeVQ6Nx0FZHU-_VFeIRyL1Gf8g_z1cTEP0cj8,11
|
|
11
|
+
agent_card_starter-0.2.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
agent_card
|