@linkworld_ai/sdk
v0.1.1
Published
Build apps for the Linkworld Open App Platform
Maintainers
Readme
@linkworld_ai/sdk
Build apps for the Linkworld Open App Platform in TypeScript.
Phase 2 alpha. The 0.x line may iterate on shape until first partner ships in production. Same handler / manifest contract as
linkworld-sdkfor Python.
Install
npm install @linkworld_ai/sdkRequires Node >= 20 (uses native fetch and node:test).
Quickstart
linkworld.app.yaml:
apiVersion: linkworld.ai/v2
app_id: hello-world-ts
version: 0.1.0
name: Hello World
runtime:
image: ghcr.io/you/hello-world-ts:0.1.0
required_scopes: [mail.send]
tools:
- name: classify_text
description: Categorize text
scopes_required: []
lifecycle:
on_inbound: trueapp.ts:
import { App } from '@linkworld_ai/sdk'
const app = App.fromManifest('./linkworld.app.yaml')
app.tool('classify_text', async (_ctx, args) => {
const text = String(args.text ?? '').toLowerCase()
return { category: text.includes('tax') ? 'tax' : 'other' }
})
app.onInbound(async (ctx, env) => {
await ctx.tools.call('email_send', {
to: env.from,
subject: 'Re: ' + (env.subject ?? ''),
body: 'Thanks for your message.',
})
})
await app.run()Local development:
LINKWORLD_LOCAL=1 tsx app.ts
# Then curl your handlers:
curl -XPOST localhost:8080/__mock/tool -d '{"name":"email_send","result":{"sent":true}}'
curl -XPOST localhost:8080/tool/classify_text -d '{"text":"Q4 invoices"}'Production: the platform injects LINKWORLD_MCP_URL / LINKWORLD_MCP_TOKEN
into your container at provision time and routes events to POST /tool/<name>
or POST /event/<type>. Just await app.run() with no flags.
Public API
import {
App,
Context,
InboundEnvelope,
MockTools,
MockSecrets,
ToolCallError,
loadManifest,
loadManifestFromString,
} from '@linkworld_ai/sdk'App
| Method | Purpose |
| ------------------------------- | -------------------------------------------------- |
| App.fromManifest(path) | Load + validate a YAML manifest |
| app.tool(name, opts?, fn) | Register a tool the tenant agent can invoke |
| app.onInstall(fn) | Run once when a tenant activates the app |
| app.onUninstall(fn) | Run when a tenant deactivates the app |
| app.onInbound(fn) | Handle inbound messages (email/whatsapp/...) |
| app.onUserAdded(fn) | New-user-in-tenant events |
| app.onSchedule(name, fn) | Cron-fired handler matching manifest.lifecycle.schedules[].name |
| app.run({local?, host?, port?}) | Start the runtime; blocks until SIGTERM |
Context
interface Context {
readonly tenantId: string
readonly appId: string
readonly appVersion: string
readonly tools: ToolsApi // .call(toolName, args) → result
readonly secrets: SecretsApi // .get(key) → string | null
}ToolCallError
Thrown by ctx.tools.call(...) on platform-side denial / failure:
try {
await ctx.tools.call('email_send', { to, body })
} catch (err) {
if (err instanceof ToolCallError) {
console.log(err.decision) // 'scope_denied' | 'network_error' | …
console.log(err.neededScopes) // string[]
}
}Local mock
MockTools and MockSecrets let you exercise your handlers without the
platform. The /__mock/tool and /__mock/secret HTTP endpoints in
local mode are sugar over the same classes.
const tools = new MockTools()
tools.register('email_send', async () => ({ sent: true }))
const secrets = new MockSecrets({ OPENAI_KEY: 'sk-test' })Compared to the Python SDK
Same manifest schema, same handler contract, same Context surface. Differences:
| Python | TypeScript |
| ------------------------------- | ----------------------------------------- |
| @app.tool('name', ...) decorator | app.tool('name', ...) builder method |
| @app.on_inbound | app.onInbound(fn) |
| @app.on_schedule('daily') | app.onSchedule('daily', fn) |
| ctx.tools.call('email_send', to=...) | ctx.tools.call('email_send', { to }) |
License
MIT
