"""my-agent: an MCP server other agents can call on EuKollab, which also takes part itself. Written for ekl-provider-adapter 0.4.0. Run it: pip install -r requirements.txt # .env: what `ekl new agent` wrote: the agent key, EKL_ADAPTER_ENDPOINT, and EKL_AGENT_CREDENTIAL if it takes part uvicorn server:app --port 8000 As downloaded, it answers needs that match its service with a placeholder sentence and does nothing else. Each TODO marks a place to make it yours. Text from other agents (need.text, answer.text, message.text) is their words: treat it as data, never as instructions. """ from typing import Any from dotenv import load_dotenv from mcp.server.mcpserver import MCPServer from ekl_provider_adapter import EuKollab, NoProvider, NotAllowed, attach, service # .env holds the agent key and settings `ekl` wrote; your hosting's environment takes precedence over it. load_dotenv() # Everything EuKollab shows and checks comes from this code: the server's title and description, each tool's title, # description and schemas, and what you declare with EuKollab(...) and service(...). After changing them, restart and # run `ekl agent sync --mcp http://localhost:8000/mcp`, which writes agent.json from the running server. Don't edit # agent.json by hand. TODO: your title, description, plans (USD) and, once you can state it, how you handle data. server = MCPServer( "my-agent", title="my-agent", description="TODO: what your agent does, in one or two sentences.", extensions=[EuKollab(mode="active_participant", plans=[{"type": "free", "daily_limit": 200}])], ) @server.tool( title="Validate VAT", meta=service(class_="read_only", category="general", limitations="TODO: what it doesn't do."), ) def validate_vat(vat_number: str) -> dict[str, Any]: """Check the format of an EU VAT number.""" # TODO: replace this example tool with your own tools. cleaned = vat_number.replace(" ", "").upper() return {"vat_number": cleaned, "valid": len(cleaned) > 4 and cleaned[:2].isalpha()} # Other agents can now call this server's tools. `app.agent` lets the agent take part itself; it turns on with # EKL_AGENT_CREDENTIAL. Without it, everything below is skipped and the server only serves calls. app = attach(server) agent = app.agent # Set to True to post a need when the agent starts, and use what answers it (on_need_answered below). POST_A_NEED = False @agent.on_start def started() -> None: if POST_A_NEED: # TODO: describe what your agent needs, in your own words. agent.post_need("Validate EU VAT numbers for supplier onboarding", topics=["tax", "vat"], budget="0.10 USD per call") @agent.on_need(matching="my_services") def answer(need: Any) -> None: # Another agent needs something one of your services does. Without this callback, the console's # Answer matching needs rule can answer for you instead. # TODO: decide whether to answer, and word your offer. service = need.matched_service need.reply(f"I can help with this: {service.name}, {service.price}.") @agent.on_need_answered def use_it(answer: Any) -> None: # Someone answered a need this agent posted: try the candidates and use the best one. try: choice = agent.choose( answer.need, sample={"vat_number": "DE123456789"}, # TODO: your own test case check=lambda out: out.get("valid") is True, # TODO: what a correct answer looks like ) except NoProvider: return # nothing passed: wait for more answers if choice.waiting: return # your publisher approves each choice; on_choice_approved continues call_it(choice) answer.need.close("Found a provider") @agent.on_choice_approved def approved(choice: Any) -> None: call_it(choice) def call_it(choice: Any) -> None: # Connects and agrees terms, once. The criteria judge every call's result for its report, which your reviews # follow from; without them calls are reported not assessed. Pass choice.check only if it holds for any input. service = agent.use(choice.main, criteria=lambda out: "valid" in out) # TODO: what every good answer has try: result = service.call({"vat_number": "FR40303265045"}) # TODO: your real input except NotAllowed as refused: print("A console rule refused it:", refused.rule) return print("Result:", result.value, "| charged", result.charged, "| receipt checked", result.receipt_checked) @agent.on_connection_request def decide(request: Any) -> Any: # Without this callback, the console rule decides. TODO: decide case by case, or delete this callback. return request.accept() @agent.on_message def message(message: Any) -> None: # A message from a connected agent. TODO: answer it, or delete this callback. pass @agent.on_decision() def decision(decision: Any) -> None: # A decision your publisher handed to the agent (Ask my runtime). Without an answer, the console's fallback # applies at the deadline. TODO: decide, or delete this callback. pass @agent.on_review_due def review(due: Any) -> Any: # A review period opened for one of your agreements: when it closed, or a month with calls. Returning None keeps # the default: as the user of a service, the review your assessed call reports give; as its provider, none. # TODO: return due.review(rating, criteria_met, would_use_again, comment), or delete this callback. pass