gp

PDDL planning solver exposed as an MCP server and Go library.
gp implements the Graphplan algorithm (Blum & Furst 1997) for
STRIPS and ADL planning domains. It ships as:
gp-server -- an MCP server that any AI assistant can call
graphplan -- a Go library for embedding planning in your apps
pddl -- a PDDL parser (STRIPS + ADL) that feeds into the solver
Background
What is automated planning?
Automated planning finds a sequence of actions that transforms an
initial state into a goal state. You describe what you want -- not
how to get there.
A planning problem has three parts:
- Domain -- the types of objects and actions available
- Problem -- the specific objects, starting state, and goal
- Plan -- the output: a sequence of grounded actions
PDDL: Planning Domain Definition Language
PDDL is the standard input format for planners. A domain defines
predicates (facts about the world) and operators (actions with
preconditions and effects):
(define (domain logistics)
(:requirements :strips :typing)
(:types city cargo rocket)
(:predicates (at ?x - cargo ?c - city)
(in ?x - cargo ?r - rocket)
(rocket-at ?r - rocket ?c - city)
(has-fuel ?r - rocket))
(:action load
:parameters (?c - cargo ?r - rocket ?loc - city)
:precondition (and (at ?c ?loc) (rocket-at ?r ?loc))
:effect (and (not (at ?c ?loc)) (in ?c ?r)))
;; ... more actions
)
A problem specifies objects, initial facts, and goals:
(define (problem deliver)
(:domain logistics)
(:objects r1 - rocket a b - cargo
london paris - city)
(:init (at a london) (at b london)
(rocket-at r1 london) (has-fuel r1))
(:goal (and (at a paris) (at b paris))))
The Graphplan algorithm
Graphplan (Blum & Furst 1997) builds a layered planning graph
alternating between proposition levels and action levels. At each
level it tracks mutual exclusion (mutex) constraints -- pairs of
actions or propositions that cannot coexist.
The algorithm:
- Extend the graph one level (add applicable actions, compute
new mutexes)
- Check if all goals are present and pairwise non-mutex
- Search backward through the graph for a valid plan
- Repeat until a plan is found or the graph levels off with
no solution
Key properties:
- Finds shortest parallel plans (fewest time steps)
- Guaranteed complete -- if a plan exists, it will find it
- Memoization prunes the search space across iterations
Examples
Rocket cargo transport
A rocket must transport two pieces of cargo from London to Paris.
domain.pddl
(define (domain rocket)
(:requirements :strips :typing :equality)
(:types rocket place cargo - object)
(:predicates
(at ?x - object ?y - place)
(in ?c - cargo ?r - rocket)
(has-fuel ?r - rocket))
(:action move
:parameters (?r - rocket ?from - place ?to - place)
:precondition (and
(not (= ?from ?to))
(at ?r ?from)
(has-fuel ?r))
:effect (and
(at ?r ?to)
(not (at ?r ?from))
(not (has-fuel ?r))))
(:action load
:parameters (?r - rocket ?p - place ?c - cargo)
:precondition (and (at ?r ?p) (at ?c ?p))
:effect (and
(in ?c ?r)
(not (at ?c ?p))))
(:action unload
:parameters (?r - rocket ?p - place ?c - cargo)
:precondition (and (at ?r ?p) (in ?c ?r))
:effect (and
(at ?c ?p)
(not (in ?c ?r)))))
problem.pddl
(define (problem rocket-2cargo)
(:domain rocket)
(:objects
r1 - rocket
london paris - place
a b - cargo)
(:init
(at r1 london)
(at a london)
(at b london)
(has-fuel r1))
(:goal (and
(at a paris)
(at b paris))))
Plan (3 time steps):
Step 0: load(r1, london, a) load(r1, london, b)
Step 1: move(r1, london, paris)
Step 2: unload(r1, paris, a) unload(r1, paris, b)
Both cargo items load in parallel, the rocket moves, and both
unload in parallel -- the shortest possible plan.
Blocks world (Sussman anomaly)
The classic AI planning benchmark. Three blocks must be stacked
in order, but the initial configuration forces the planner to
undo partial progress.
domain.pddl
(define (domain blocks-world)
(:requirements :strips :typing)
(:types block - object)
(:predicates
(on ?x - block ?y - block)
(on-table ?x - block)
(clear ?x - block)
(holding ?x - block)
(arm-empty))
(:action pick-up
:parameters (?x - block)
:precondition (and
(clear ?x)
(on-table ?x)
(arm-empty))
:effect (and
(holding ?x)
(not (on-table ?x))
(not (clear ?x))
(not (arm-empty))))
(:action put-down
:parameters (?x - block)
:precondition (holding ?x)
:effect (and
(on-table ?x)
(clear ?x)
(arm-empty)
(not (holding ?x))))
(:action stack
:parameters (?x - block ?y - block)
:precondition (and
(holding ?x)
(clear ?y))
:effect (and
(on ?x ?y)
(clear ?x)
(arm-empty)
(not (holding ?x))
(not (clear ?y))))
(:action unstack
:parameters (?x - block ?y - block)
:precondition (and
(on ?x ?y)
(clear ?x)
(arm-empty))
:effect (and
(holding ?x)
(clear ?y)
(not (on ?x ?y))
(not (clear ?x))
(not (arm-empty)))))
problem.pddl (Sussman anomaly)
(define (problem sussman-anomaly)
(:domain blocks-world)
(:objects a b c - block)
(:init
(on c a)
(on-table a)
(on-table b)
(clear c)
(clear b)
(arm-empty))
(:goal (and
(on a b)
(on b c))))
Initial state: C is on A, A and B are on the table.
Goal: A on B, B on C (stack: A-B-C from top to bottom).
This is the Sussman anomaly -- you cannot achieve both subgoals
independently without undoing one. The planner must unstack C
from A before building the target stack.
Emergency response (ADL features)
An emergency domain that combines several ADL features:
- Derived predicate with
exists -- can-handle is true when
a qualified, available responder exists
- Disjunctive precondition --
dispatch requires the responder
to be at the zone or backup to be available
- Existential precondition --
sound-alarm fires when any
incident exists in the zone
- Universal conditional effect --
sound-alarm alerts every
responder present in the zone via forall/when
domain.pddl
(define (domain emergency)
(:requirements
:strips :typing :derived-predicates
:disjunctive-preconditions
:universal-preconditions :conditional-effects)
(:types
responder incident zone - object)
(:predicates
(qualified ?r - responder ?i - incident)
(available ?r - responder)
(at-zone ?r - responder ?z - zone)
(incident-at ?i - incident ?z - zone)
(dispatched ?r - responder ?i - incident)
(alerted ?r - responder)
(can-handle ?i - incident)
(alarm-sounded ?z - zone)
(backup-available ?z - zone))
(:derived (can-handle ?i - incident)
(exists (?r - responder)
(and (qualified ?r ?i)
(available ?r))))
(:action dispatch
:parameters
(?r - responder ?i - incident ?z - zone)
:precondition (and
(qualified ?r ?i)
(available ?r)
(incident-at ?i ?z)
(or (at-zone ?r ?z) (backup-available ?z)))
:effect (and
(dispatched ?r ?i)
(not (available ?r))))
(:action sound-alarm
:parameters (?z - zone)
:precondition
(exists (?i - incident) (incident-at ?i ?z))
:effect (and
(alarm-sounded ?z)
(forall (?r - responder)
(when (at-zone ?r ?z) (alerted ?r)))))
(:action call-backup
:parameters (?z - zone)
:precondition (alarm-sounded ?z)
:effect (backup-available ?z)))
problem.pddl
(define (problem respond-to-fire)
(:domain emergency)
(:objects
medic1 fire1-resp - responder
fire1 - incident
zone-a zone-b - zone)
(:init
(qualified medic1 fire1)
(qualified fire1-resp fire1)
(available medic1)
(available fire1-resp)
(at-zone medic1 zone-a)
(at-zone fire1-resp zone-b)
(incident-at fire1 zone-a))
(:goal (and
(dispatched medic1 fire1)
(alerted medic1)
(alarm-sounded zone-a))))
Plan (1 time step):
Step 0: dispatch(medic1, fire1, zone-a) sound-alarm(zone-a)
The planner dispatches the medic and sounds the alarm in parallel.
sound-alarm uses forall/when to alert every responder in
zone-a (medic1), while dispatch uses the or precondition --
medic1 is already at zone-a so backup is not needed.
Installation
Binary (recommended)
Download the latest release for your platform:
Linux / macOS:
curl -sSL https://raw.githubusercontent.com/byte4ever/gp/master/install.sh | sh
Windows (PowerShell):
irm https://raw.githubusercontent.com/byte4ever/gp/master/install.ps1 | iex
Verify:
gp-server --version
Build from source
go install github.com/byte4ever/gp/cmd/gp-server@latest
Requires Go 1.25+.
MCP Configuration
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"gp": {
"command": "gp-server"
}
}
}
Claude Code
Add to your project's .mcp.json:
{
"mcpServers": {
"gp": {
"command": "gp-server",
"type": "stdio"
}
}
}
Or run directly:
claude mcp add gp gp-server
Other MCP clients
gp-server speaks MCP over stdio. Point any MCP-compatible client
at the gp-server binary with no arguments.
One-shot solving
| Tool |
Parameters |
Description |
solve_problem |
format (pddl or json), domain, problem |
Parse and solve an ADL planning problem in a single call. |
Session-based solving
For multi-turn workflows where the AI assistant builds up the
domain and problem incrementally:
| Tool |
Parameters |
Description |
create_session |
name |
Create a named planning session. |
set_domain |
session, domain |
Set the PDDL domain on a session. |
set_problem |
session, problem |
Set the PDDL problem on a session. |
solve_session |
session |
Solve the problem stored in a session. |
list_sessions |
-- |
List all active session names. |
delete_session |
session |
Delete a session by name. |
All solve tools return a list of time steps. Actions within the
same step are guaranteed non-interfering and can be executed
in parallel. Steps must be executed sequentially:
{
"solvable": true,
"steps": [
{"time": 0, "actions": ["load(r1, london, a)", "load(r1, london, b)"]},
{"time": 1, "actions": ["move(r1, london, paris)"]},
{"time": 2, "actions": ["unload(r1, paris, a)", "unload(r1, paris, b)"]}
]
}
When no plan exists:
{
"solvable": false,
"error": "no valid plan found"
}
Go Library
Three packages, each usable independently:
go get github.com/byte4ever/gp
graphplan -- solver engine (STRIPS + ADL)
import "github.com/byte4ever/gp/graphplan"
Build domain and problem structs programmatically, then solve:
solver, err := graphplan.NewSolver(nil)
plan, err := solver.Solve(problem)
See graphplan/README.md for full API.
pddl -- parser
import "github.com/byte4ever/gp/pddl"
Parse PDDL strings into graphplan types:
adapter := pddl.NewAdapter()
domain, err := adapter.ParseDomain(domainPDDL)
problem, err := adapter.ParseProblem(problemPDDL, domain)
Supports :strips, :typing, :equality, :disjunctive-preconditions,
:conditional-effects, :quantified-preconditions, and
:derived-predicates requirements.
See pddl/README.md for full API.
mcpserver -- MCP transport
import "github.com/byte4ever/gp/mcpserver"
Embed the MCP server in your own application:
srv, err := mcpserver.NewServer(cfg, solver, adapter, adapter)
srv.Run()
See mcpserver/README.md for full API.
Project Structure
cmd/gp-server/ Entrypoint binary
graphplan/ Solver engine (Graphplan algorithm)
pddl/ PDDL parser and AST-to-graphplan converter
mcpserver/ MCP server wiring and tool handlers
testdata/ PDDL test fixtures
docs/ Design and implementation plans
install.sh Linux/macOS installer
install.ps1 Windows installer
.goreleaser.yaml Cross-platform release config
License
MIT