Velin

July 29, 2026 · View on GitHub

Logo of Velin

Velin

npm version npm downloads bundle JSDocs License

Have you wondered how it feels if you can develop the prompts of agents and MCP servers with the power of Vue or React?

Develop prompts with Vue SFC, React components, TSX/JSX source, or Markdown like a pro.

Table of Contents

Framework Support

Vue logo      React logo      Svelte logo

Vue supported      React supported      Svelte in progress

We got a playground too, check it out:

Light Theme Playground

Quick Start

Try it by running following command under your pnpm/npm project.

# For browser users
npm i @velin-dev/vue
npm i @velin-dev/react

# For Node.js, CI, server rendering and backend users
npm i @velin-dev/core-vue
npm i @velin-dev/core-react

Features

  • No longer need to fight and format with the non-supported DSL of templating language!
  • Use HTML elements like <div> for block elements, <span> for inline elements.
  • Directives with native Vue template syntax, such as v-if and v-else.
  • React component rendering with Node.js and browser-compatible entry points.
  • Trusted TSX/JSX source loading for React prompt components.
  • Compositing other open sourced prompt components or composables over memory systems.

All included...

How it feels

Vue SFC

<!-- Prompt.vue -->
<script setup lang="ts">
defineProps<{
  name: string
}>()
</script>

<template>
  <div>
    Hello world, this is {{ name }}!
  </div>
</template>

In Node.js

import { readFile } from 'node:fs/promises'

import { renderSFCString } from '@velin-dev/core-vue'
import { ref } from 'vue'

const source = await readFile('./Prompt.vue', 'utf-8')
const name = ref<string>('Velin')
const { rendered } = await renderSFCString(source, { name })

console.log(rendered)
// Hello world, this is Velin!

In Vue / Browser

<script setup lang="ts">
import { usePrompt } from '@velin-dev/vue'
import { ref, watch } from 'vue'

import Prompt from './Prompt.vue'

const name = ref<string>('Velin')
const { prompt } = usePrompt(Prompt, { name })

watch(prompt, () => {
  console.log(prompt)
  // Hello world, this is Velin!
})
</script>

React component

import { renderComponent } from '@velin-dev/core-react'

function Prompt({ name }: { name: string }) {
  return (
    <article>
      <h1>{`Hello ${name}`}</h1>
      <p>Render React components as Markdown prompts.</p>
    </article>
  )
}

const markdown = await renderComponent(Prompt, { name: 'Velin' })

console.log(markdown)
// # Hello Velin
//
// Render React components as Markdown prompts.

In React / Browser

import { usePrompt } from '@velin-dev/react'

function Prompt({ name }: { name: string }) {
  return <div>{`Hello ${name}`}</div>
}

function PromptPreview() {
  const { prompt, rendering, dispose } = usePrompt(Prompt, { name: 'Velin' })

  return (
    <>
      <pre>{rendering ? 'Rendering...' : prompt}</pre>
      <button type="button" onClick={dispose}>Dispose</button>
    </>
  )
}

From trusted TSX/JSX source

import { renderComponent } from '@velin-dev/core-react'
import { componentFromSource } from '@velin-dev/source-react'

const Prompt = await componentFromSource<{ name: string }>(`
  export default function Prompt({ name }: { name: string }) {
    return <div>Hello {name}</div>
  }
`)

const markdown = await renderComponent(Prompt, { name: 'Velin' })

componentFromSource and componentFromFile evaluate transformed ESM and are not a sandbox or security boundary. Use them only with trusted source.

As an LLM tool (Vue)

A prompt component is already most of a tool: its props are the arguments, its script does the work, and what it renders is the answer. defineTool closes the gap by deriving the argument schema from the props, so the same component can be handed to a model.

<!-- GetWeather.vue -->
<script setup lang="ts">
import { onServerPrefetch, ref } from 'vue'

const props = defineProps<{ city: string }>()

const forecast = ref('')
onServerPrefetch(async () => {
  forecast.value = await fetch(`https://example.com/weather/${props.city}`).then(r => r.text())
})
</script>

<template>
  <div>The weather in {{ city }} is {{ forecast }}.</div>
</template>
import { defineTool } from '@velin-dev/core-vue'

import GetWeather from './GetWeather.vue'

const tool = defineTool(GetWeather, { description: 'Get the current weather for a city.' })

tool.function.parameters
// { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], additionalProperties: false }

Rendering goes through Vue's server renderer, so onServerPrefetch and async setup are awaited before the result comes back.

Runners that execute tools themselves take the tool as-is:

import { generateText } from '@xsai/generate-text'

await generateText({ ...options, messages, tools: [tool] })

With an SDK that only sends the definition, the tool serializes to the wire object — execute is a function, so it drops out — and closes the loop in one call:

const completion = await openai.chat.completions.create({ model, messages, tools: [tool] })
const call = completion.choices[0].message.tool_calls?.[0]

if (call) {
  // `arguments` arrives as a JSON string; `execute` accepts it directly.
  const content = await tool.execute(call.function.arguments, { toolCallId: call.id })
  messages.push({ role: 'tool', tool_call_id: call.id, content })
}

In a component, useTool from @velin-dev/vue returns the same tool plus a pending ref for the in-flight state.

Arguments the component does not declare are dropped, and missing or mistyped ones raise a VelinToolInputError whose message is written to be handed back to the model.

Similar projects

Development

Clone

git clone https://github.com/moeru-ai/velin.git
cd velin

Install dependencies

corepack enable
pnpm install

Note

We would recommend to install @antfu/ni to make your script simpler.

corepack enable
npm i -g @antfu/ni

Once installed, you can

  • use ni for pnpm install, npm install and yarn install.
  • use nr for pnpm run, npm run and yarn run.

You don't need to care about the package manager, ni will help you choose the right one.

pnpm dev

Note

For @antfu/ni users, you can

nr dev

Build

pnpm build

Note

For @antfu/ni users, you can

nr build

Documentation

The table of contents above is generated with doctoc, following the same README maintenance pattern used in moeru-ai/alint. Run this after changing README headings:

pnpm docs:update

docs:update refreshes the root README TOC. Velin keeps package-level READMEs separate because each published package documents a different framework or runtime entry point.

License

MIT