Developers
MCP is the API. Connect Claude, ChatGPT or your coding agent, and build on plain files you can read.
For you
Add Ghostlyn to Claude, ChatGPT or your coding agent.
For your coding agent
Paste this into Claude Code, Codex or OpenCode.
Then restart your agent and sign in.
Architecture
A small core runs the agent. Everything a team sees lives in bundles on top of it. Click a part to read what it does.
Open standards
What you write here works in your other agents, and theirs works here.
AGENTS.md
agents.mdA project's standing instructions, in the file Codex and Cursor already read. A folder's own AGENTS.md adds rules where it applies.
Read moreAgent Skills
agentskills.ioEach skill is a folder with a SKILL.md, scripts and references, so a skill moves between Claude Code and Ghostlyn unchanged.
Read moreGhostlyn is a server that Claude, ChatGPT and coding agents connect to, and a client of any MCP connector you add.
Read moreMCP Apps
modelcontextprotocol.ioA connected server's tools can ship their own screens, shown in the chat in a sandboxed frame.
Read moreAgent Plugins
agent-plugins.orgA bundle keeps skills in a plugin's skills/ layout, so an imported plugin brings its skills over. Add its MCP servers as connectors.
Read moreData and models
Where your data sits, which models see it and what leaves with a connected client.
- Stored
- Hosted in Germany, in the EU
- Inference
- Anthropic, OpenAI and others, with an EU-only route
- Ownership
- Bundles export as files, sync to your Git
What a connected client reads enters its own model under its provider's terms.
Privacy and accessMCP server
Two endpoints, OAuth sign-in with no key, and a scope for every tool.
OAuth 2.1 with dynamic client registration. The member approves each scope, and revokes the client from Settings, MCP.
| Scope | Grants |
|---|---|
| records:read | Search past chats and records recall |
| tables:read | Query table rows query |
| tables:write | Add, change and remove table rows save remove |
| volumes:read | Read files in projects you can open read glob grep lookup |
| volumes:write | Write, move and delete those files write edit delete move |
| web:fetch | Fetch pages and search the web as Ghostlyn fetch curl search |
| skills:list | List skills you can run find |
| skills:invoke | Run a skill |
| agent:invoke | Hand a task to the Ghostlyn agent invoke |
Rate limits
Per member and per address. Ask support to raise them for your team.
| Tool calls per member | 60 a minute, 600 an hour |
| Sign-in requests per address | 120 a minute, 1,200 an hour |
| Client registrations per address | 60 an hour |
Each connected client, what it may do and Revoke sit in your settings.Open MCP settings
Your tools
Bring what you already run into Ghostlyn. Each links to the feature it belongs to.
Webhooks take a 64 KB body and start up to 25 runs per webhook a day.
Small core
The core runs the loop, the sandbox and storage. Connectors, skills and apps are bundles you can read and change.
Read
Every built-in bundle opens in the app, and the agent reads it too.
Copy
Take a copy of any bundle into your organization, built-ins included.
Change
Edit the copy yourself or ask the agent to. It runs the moment it saves.
28 built-in bundles
5 in the core, 23 extendedImprove a built-in
File a bug or a fix for any built-in bundle on GitHub. Your coding agent can do it for you.
gh issue create -R ghostlyn/ghostlyn
Bundles
A bundle is plain files: a manifest, tools in TypeScript and skills in the Agent Skills format.
solidtime/
import { jsonClient } from "ghostlyn/http";
import {
dateRangeUtc,
rollup,
type SolidtimeProject,
type SolidtimeClient,
} from "./summary.ts";
export const instructions = `solidtime tracks time entries for an organization, by person, client and
project. Call summary with a start and end date, inclusive, and an IANA
timezone, to get totals for that range. Name organization only when the
member belongs to more than one solidtime organization.`;
const client = jsonClient({ credential: "solidtime" });
type Membership = {
id: string;
role: string;
organization: { id: string; name: string };
};
type PageMeta = { last_page?: number };
async function pageAll<T>(path: string): Promise<T[]> {
const results: T[] = [];
let page = 1;
for (;;) {
const sep = path.includes("?") ? "&" : "?";
const res = await client("GET", `${path}${sep}page=${page}`);
const data = (res.data as T[] | undefined) ?? [];
results.push(...data);
const meta = res.meta as PageMeta | undefined;
const lastPage = meta?.last_page ?? 1;
if (page >= lastPage) break;
page++;
}
return results;
}
function pickMembership(
memberships: Membership[],
organization: string | undefined
): Membership {
if (memberships.length === 0) {
throw new Error("No solidtime organization membership found.");
}
const names = memberships.map(m => m.organization.name).join(", ");
if (organization) {
const match = memberships.find(
m =>
m.organization.name === organization ||
m.organization.id === organization
);
if (!match) {
throw new Error(
`No solidtime organization named "${organization}". You belong to: ${names}.`
);
}
return match;
}
if (memberships.length === 1) return memberships[0];
throw new Error(
`You belong to more than one solidtime organization. Name one: ${names}.`
);
}
/**
* Summarizes tracked solidtime hours by person, client within each person and
* project within each client, plus team totals per client, for an inclusive
* date range.
*
* @messageTemplate Summarizing solidtime hours
* @readOnly
*/
export async function summary({
start,
end,
timezone,
organization,
}: {
/** Inclusive start date, YYYY-MM-DD. */
start: string;
/** Inclusive end date, YYYY-MM-DD. */
end: string;
/** IANA timezone the range is local to, such as Europe/Helsinki. */
timezone: string;
/** Organization name or id, needed only with more than one membership. */
organization?: string;
}) {
const memberships = await client("GET", "/api/v1/users/me/memberships");
const membership = pickMembership(
(memberships.data as Membership[] | undefined) ?? [],
organization
);
const orgId = membership.organization.id;
const restrictedToSelf = membership.role === "employee";
const range = dateRangeUtc(start, end, timezone);
const params = new URLSearchParams({
group: "user",
sub_group: "project",
start: range.start,
end: range.end,
});
if (restrictedToSelf) params.set("member_id", membership.id);
const [aggregate, projects, clients] = await Promise.all([
client(
"GET",
`/api/v1/organizations/${orgId}/time-entries/aggregate?${params}`
),
pageAll<SolidtimeProject>(`/api/v1/organizations/${orgId}/projects`),
pageAll<SolidtimeClient>(`/api/v1/organizations/${orgId}/clients`),
]);
const result = rollup(
aggregate.data as Parameters<typeof rollup>[0],
projects,
clients
);
return {
organization: membership.organization.name,
start,
end,
restrictedToSelf,
...result,
};
}Read from bundles/extended/solidtime when the page builds, so the example always runs.
Sandbox
Tools and scripts run isolated, with a fixed set of modules. Click one for what it does.
- Runtime
- QuickJS compiled to WebAssembly
- Each run
- 30 seconds, 1 MB of output, 50 nested tool calls
- Network
- Only through ghostlyn/http, with host checks
- Credentials
- Named, injected only toward their own host
- Not available
- Filesystem, sockets, child processes
Deno standard library
Node built-ins
npm packages
Ghostlyn
Docs
The references Ghostlyn's own support agent reads, published as they are.
- Working with the agentHow the agent behaves in a conversation, what it reads and the code it runs
- CustomizeHow projects, skills, apps and connectors work, and the files inside them
- Privacy and accessWho can see your chats and data, where it is stored and where it goes
- Feature limits and mechanicsEach feature's caps, timeouts and the rules it enforces
- TroubleshootingSymptom by symptom fixes for common problems
Updates
What shipped, and whether Ghostlyn is running right now.
