@panagos/jojoscript 0.9.0 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/package.json +1 -1
- package/src/index.js +33 -33
- package/src/pipeline-parser.js +44 -0
- package/src/runtime/index.js +5 -0
- package/src/runtime/mcp.js +93 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# JojoScript 0.9.
|
|
1
|
+
# JojoScript 0.9.3
|
|
2
2
|
|
|
3
3
|
JojoScript is a small JavaScript-compatible language and runtime for
|
|
4
4
|
expressive, composable pipelines. It adds lightweight syntax for declarations,
|
|
@@ -6,8 +6,8 @@ functions, pattern matching, type annotations, and pipelines while preserving
|
|
|
6
6
|
ordinary JavaScript. Its compiler uses a lightweight lexer and source
|
|
7
7
|
transformations rather than a full AST, keeping the toolchain compact. The
|
|
8
8
|
project also provides lazy and async collection operators, checkpointing,
|
|
9
|
-
retries, metrics, pipeline graphing, CLI workflows, effect handlers,
|
|
10
|
-
VS Code editor support.
|
|
9
|
+
retries, metrics, pipeline graphing, CLI workflows, effect handlers, MCP
|
|
10
|
+
support, and VS Code editor support.
|
|
11
11
|
|
|
12
12
|
See [CHANGELOG.md](CHANGELOG.md) for release history and
|
|
13
13
|
[example/](example/) for a complete runnable application that exercises
|
|
@@ -237,6 +237,26 @@ becomes:
|
|
|
237
237
|
divide(100, value)
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
#### Property access stages
|
|
241
|
+
|
|
242
|
+
A pipeline stage may also start with a dot to read a field or invoke a method
|
|
243
|
+
on the piped value directly:
|
|
244
|
+
|
|
245
|
+
```jojo
|
|
246
|
+
users |> .length
|
|
247
|
+
name |> .trim() |> .toUpperCase()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
becomes:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
users.length
|
|
254
|
+
name.trim().toUpperCase()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
This is handy for compact pipelines where the previous value is the receiver of
|
|
258
|
+
an ordinary JavaScript property or method call.
|
|
259
|
+
|
|
240
260
|
#### `catch` / `orElse` control stages
|
|
241
261
|
|
|
242
262
|
`catch` and `orElse` provide inline error handling without leaving pipeline
|
|
@@ -719,6 +739,29 @@ never through `|>`) and also uses pipeline syntax with overlapping names,
|
|
|
719
739
|
the compiler merges the auto-imported names into the existing import
|
|
720
740
|
instead of emitting a second, colliding import declaration.
|
|
721
741
|
|
|
742
|
+
### MCP support
|
|
743
|
+
|
|
744
|
+
JojoScript includes a small MCP-style JSON-RPC runtime for Node.js. It does
|
|
745
|
+
not add MCP-specific language syntax; tool implementations remain ordinary
|
|
746
|
+
JavaScript functions.
|
|
747
|
+
|
|
748
|
+
```js
|
|
749
|
+
import { createMcpServer, runMcpStdioServer } from "jojoscript/runtime"
|
|
750
|
+
|
|
751
|
+
const server = createMcpServer({
|
|
752
|
+
tools: {
|
|
753
|
+
echo: args => ({ content: [{ type: "text", text: args.value }] })
|
|
754
|
+
}
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
await runMcpStdioServer(server)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
The server accepts `tools/call` requests and returns JSON-RPC responses on
|
|
761
|
+
standard output. It is intentionally minimal: supporting tool registration,
|
|
762
|
+
request dispatch, structured errors, and the stdin/stdout transport without any
|
|
763
|
+
external NPM packages.
|
|
764
|
+
|
|
722
765
|
## Example
|
|
723
766
|
|
|
724
767
|
See [example/](example/). It is a complete runnable project covering every
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
export {
|
|
2
|
-
compile,
|
|
3
|
-
compileFile,
|
|
4
|
-
compileDirectory
|
|
5
|
-
} from './compiler.js'
|
|
6
|
-
|
|
7
|
-
export {
|
|
8
|
-
transformPipelines,
|
|
9
|
-
emitPipeline,
|
|
10
|
-
findPipelineStdlibFunctions
|
|
11
|
-
} from './pipeline-parser.js'
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
transformMatchExpressions
|
|
15
|
-
} from './match-parser.js'
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
transformTypeAnnotations
|
|
19
|
-
} from './type-annotations.js'
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
analyzePipelines,
|
|
23
|
-
buildGraph,
|
|
24
|
-
renderGraphText,
|
|
25
|
-
renderGraphDot
|
|
26
|
-
} from './pipeline-ir.js'
|
|
27
|
-
|
|
28
|
-
export {
|
|
29
|
-
transformEffectDeclarations,
|
|
30
|
-
transformHandleBlocks,
|
|
31
|
-
findEffectDeclarations,
|
|
32
|
-
findHandleBlocks
|
|
33
|
-
} from './effect-parser.js'
|
|
1
|
+
export {
|
|
2
|
+
compile,
|
|
3
|
+
compileFile,
|
|
4
|
+
compileDirectory
|
|
5
|
+
} from './compiler.js'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
transformPipelines,
|
|
9
|
+
emitPipeline,
|
|
10
|
+
findPipelineStdlibFunctions
|
|
11
|
+
} from './pipeline-parser.js'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
transformMatchExpressions
|
|
15
|
+
} from './match-parser.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
transformTypeAnnotations
|
|
19
|
+
} from './type-annotations.js'
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
analyzePipelines,
|
|
23
|
+
buildGraph,
|
|
24
|
+
renderGraphText,
|
|
25
|
+
renderGraphDot
|
|
26
|
+
} from './pipeline-ir.js'
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
transformEffectDeclarations,
|
|
30
|
+
transformHandleBlocks,
|
|
31
|
+
findEffectDeclarations,
|
|
32
|
+
findHandleBlocks
|
|
33
|
+
} from './effect-parser.js'
|
package/src/pipeline-parser.js
CHANGED
|
@@ -112,9 +112,18 @@ function splitArguments(tokens) {
|
|
|
112
112
|
return result
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function isPropertyAccessName(name) {
|
|
116
|
+
return /^\.[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)
|
|
117
|
+
}
|
|
118
|
+
|
|
115
119
|
function parseStage(source, tokens) {
|
|
116
120
|
if (!tokens.length) throw new SyntaxError('Empty pipeline stage')
|
|
117
121
|
|
|
122
|
+
const bareName = text(source, tokens).trim()
|
|
123
|
+
if (isPropertyAccessName(bareName)) {
|
|
124
|
+
return { name: bareName, args: [], propertyCall: false }
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
if (tokens.every((token, index) =>
|
|
119
128
|
index % 2 === 0
|
|
120
129
|
? token.type === 'identifier'
|
|
@@ -145,6 +154,14 @@ function parseStage(source, tokens) {
|
|
|
145
154
|
|
|
146
155
|
const name = text(source, tokens.slice(0, openIndex)).trim()
|
|
147
156
|
|
|
157
|
+
if (isPropertyAccessName(name)) {
|
|
158
|
+
const args = splitArguments(
|
|
159
|
+
tokens.slice(openIndex + 1, closeIndex)
|
|
160
|
+
).map(argument => text(source, argument).trim())
|
|
161
|
+
|
|
162
|
+
return { name, args, propertyCall: true }
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(name)) {
|
|
149
166
|
throw new SyntaxError(`Invalid pipeline function: ${name}`)
|
|
150
167
|
}
|
|
@@ -310,6 +327,33 @@ export function emitPipeline(pipeline, options = {}) {
|
|
|
310
327
|
continue
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
if (stage.name === 'pass') {
|
|
331
|
+
if (stage.args.length === 0) continue
|
|
332
|
+
if (stage.args.length === 1 && stage.args[0] === '_') {
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
throw new SyntaxError('the `pass` pipeline stage takes no arguments or a single `_` placeholder')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (stage.name.startsWith('.')) {
|
|
339
|
+
const args = stage.args.slice()
|
|
340
|
+
const placeholderIndex = args.indexOf('_')
|
|
341
|
+
if (placeholderIndex !== -1) {
|
|
342
|
+
args[placeholderIndex] = expression
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (stage.propertyCall || args.length > 0) {
|
|
346
|
+
expression = `${expression}${stage.name}(${args.join(', ')})`
|
|
347
|
+
} else {
|
|
348
|
+
expression = `${expression}${stage.name}`
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (options.profile) {
|
|
352
|
+
expression = `traceNode(${expression}, ${metaLiteral(pipelineId, stage.nodeId, stage.name)})`
|
|
353
|
+
}
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
|
|
313
357
|
if (stage.name === 'retry' && stage.standaloneRetry) {
|
|
314
358
|
const meta = metaLiteral(pipelineId, stage.nodeId, 'retry')
|
|
315
359
|
expression = `(await retry(async () => (${expression}), ${stage.args[0]}, ${meta})())`
|
package/src/runtime/index.js
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Minimal MCP-style JSON-RPC transport for Node.js. The server is protocol-
|
|
3
|
+
* focused rather than introducing MCP-specific JojoScript syntax; tool
|
|
4
|
+
* implementations remain ordinary JavaScript functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createInterface } from 'node:readline'
|
|
8
|
+
|
|
9
|
+
export class McpServer {
|
|
10
|
+
constructor({ tools = {} } = {}) {
|
|
11
|
+
this.tools = new Map(Object.entries(tools))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async handleRequest(message) {
|
|
15
|
+
if (!message || typeof message !== 'object') {
|
|
16
|
+
return {
|
|
17
|
+
jsonrpc: '2.0',
|
|
18
|
+
id: null,
|
|
19
|
+
error: {
|
|
20
|
+
code: -32600,
|
|
21
|
+
message: 'Invalid Request'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (message.method !== 'tools/call') {
|
|
27
|
+
return {
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
id: message.id ?? null,
|
|
30
|
+
error: {
|
|
31
|
+
code: -32601,
|
|
32
|
+
message: 'Method not found'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const toolName = message.params?.name
|
|
38
|
+
const toolArgs = message.params?.arguments ?? {}
|
|
39
|
+
const tool = this.tools.get(toolName)
|
|
40
|
+
|
|
41
|
+
if (!tool || typeof tool !== 'function') {
|
|
42
|
+
return {
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
id: message.id ?? null,
|
|
45
|
+
error: {
|
|
46
|
+
code: -32602,
|
|
47
|
+
message: `Unknown tool: ${toolName}`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await tool(toolArgs)
|
|
54
|
+
return {
|
|
55
|
+
jsonrpc: '2.0',
|
|
56
|
+
id: message.id ?? null,
|
|
57
|
+
result
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
jsonrpc: '2.0',
|
|
62
|
+
id: message.id ?? null,
|
|
63
|
+
error: {
|
|
64
|
+
code: -32603,
|
|
65
|
+
message: error instanceof Error ? error.message : String(error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createMcpServer(options = {}) {
|
|
73
|
+
return new McpServer(options)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runMcpStdioServer(server, options = {}) {
|
|
77
|
+
const input = options.input ?? process.stdin
|
|
78
|
+
const output = options.output ?? process.stdout
|
|
79
|
+
|
|
80
|
+
const lines = Array.isArray(input)
|
|
81
|
+
? input.values()
|
|
82
|
+
: createInterface({ input, crlfDelay: Infinity })
|
|
83
|
+
|
|
84
|
+
for await (const line of lines) {
|
|
85
|
+
if (!line.trim()) continue
|
|
86
|
+
|
|
87
|
+
const message = JSON.parse(line)
|
|
88
|
+
const response = await server.handleRequest(message)
|
|
89
|
+
output.write(`${JSON.stringify(response)}\n`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return 0
|
|
93
|
+
}
|