Skip to content

Commit caa43a5

Browse files
committed
Release version 1.7.16 of the tinystruct framework
feat: improve session management and refactor MCP implementation BREAKING CHANGES: - Renamed MCPServerApplication to MCPServer for consistency - Made AbstractMCPResource.executeLocally() abstract, requiring implementations - Removed registerToolMethods() - now integrated into registerTool() Session Management: - Add multi-session support with session-specific state tracking - Implement ThreadLocal-based session ID management - Add Mcp-session-id header handling for session persistence - Replace single sessionState with per-session state map - Properly cleanup session context in finally blocks Client Improvements: - Refactor HTTP communication to use URLRequest/URLResponse - Improve SSE event stream handling with functional approach - Add session ID capture and reuse across requests - Better error handling and connection management Protocol & Standards: - Update protocol version constant to PROTOCOL_VERSION - Standardize server name to "tinystruct-mcp" - Add SESSION_ID constant to Http class - Use Header enum for standard HTTP headers - Remove TOKEN_PARAM in favor of Authorization header Code Quality: - Add missing executeLocally() implementations in MCPDataResource and MCPTool - Improve documentation and formatting - Remove unused mimeType variable in resource discovery - Better separation of concerns in request handling Examples & Documentation: - Update README with new API usage patterns - Add batch request and error handling examples - Document session management features - Update all code examples to use MCPServer - Add comprehensive usage examples for CLI client
1 parent 62d3941 commit caa43a5

22 files changed

+438
-1305
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Installation and Getting Started Manually
2525
<dependency>
2626
<groupId>org.tinystruct</groupId>
2727
<artifactId>tinystruct</artifactId>
28-
<version>1.7.15</version>
28+
<version>1.7.16</version>
2929
<classifier>jar-with-dependencies</classifier> <!-- Optional -->
3030
</dependency>
3131
```
@@ -91,7 +91,7 @@ Execute in CLI mode
9191
$ bin/dispatcher --version
9292

9393
_/ ' _ _/ _ _ _/
94-
/ / /) (/ _) / / (/ ( / 1.7.15
94+
/ / /) (/ _) / / (/ ( / 1.7.16
9595
/
9696
```
9797
```tcsh

bin/dispatcher

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# ***************************************************************************
1818

1919
ROOT="$(pwd)"
20-
VERSION="1.7.15"
20+
VERSION="1.7.16"
2121
cd "$(dirname "$0")" || exit
2222
cd "../"
2323
# Navigate to the root directory

bin/dispatcher.cmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
set "MAVEN_REPO=%USERPROFILE%\.m2\repository\org\tinystruct\tinystruct"
2020
@REM Consolidate classpath entries, initialize ROOT and VERSION
2121
set "ROOT=%~dp0.."
22-
set "VERSION=1.7.15"
22+
set "VERSION=1.7.16"
2323

2424
@REM Define the paths for tinystruct jars in the Maven repository
2525
set "DEFAULT_JAR_FILE=%MAVEN_REPO%\%VERSION%\tinystruct-%VERSION%.jar"

build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ elif [ -f "$HOME/.bash_profile" ]; then
2525
fi
2626

2727
# Define variables
28-
TARGET_JAR="./target/tinystruct-1.7.15.jar"
28+
TARGET_JAR="./target/tinystruct-1.7.16.jar"
2929
NATIVE_NAME="dispatcher-native"
3030
MAIN_CLASS="org.tinystruct.system.Dispatcher"
3131
CONFIG_DIR="./bin/.metadata"

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>org.tinystruct</groupId>
66
<artifactId>tinystruct</artifactId>
7-
<version>1.7.15</version>
7+
<version>1.7.16</version>
88
<name>tinystruct framework</name>
99
<description>A lightweight, modular Java application framework for web and CLI development,
1010
designed for AI integration and plugin-based architecture.

src/main/java/org/tinystruct/mcp/AbstractMCPResource.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,5 @@ protected boolean supportsLocalExecution() {
9595
* @return The result of the execution
9696
* @throws MCPException If an error occurs during execution or if local execution is not supported
9797
*/
98-
protected Object executeLocally(Builder builder) throws MCPException {
99-
throw new MCPException("Local execution not supported for resource: " + name);
100-
}
98+
abstract protected Object executeLocally(Builder builder) throws MCPException;
10199
}

src/main/java/org/tinystruct/mcp/MCPApplication.java

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ public abstract class MCPApplication extends AbstractApplication {
2929
protected JsonRpcHandler jsonRpcHandler;
3030
protected AuthorizationHandler authHandler;
3131

32-
protected SessionState sessionState = SessionState.DISCONNECTED;
32+
protected final Map<String, SessionState> sessionStates = new ConcurrentHashMap<>();
3333
protected final Map<String, Object> sessionMap = new ConcurrentHashMap<>(); // sessionId -> user info or state
34+
private static final ThreadLocal<String> currentSessionId = new ThreadLocal<>();
3435

3536
// Generic registries for tools, resources, prompts, and custom RPC handlers
3637
protected final Map<String, MCPTool> tools = new java.util.concurrent.ConcurrentHashMap<>();
@@ -40,8 +41,10 @@ public abstract class MCPApplication extends AbstractApplication {
4041
protected static final Map<String, MCPTool.MCPToolMethod> toolMethods = new java.util.concurrent.ConcurrentHashMap<>();
4142

4243
/**
43-
* Initializes the MCP application, setting up authentication, SSE, JSON-RPC handler,
44-
* and registering core protocol handlers. Subclasses should call super.init() and
44+
* Initializes the MCP application, setting up authentication, SSE, JSON-RPC
45+
* handler,
46+
* and registering core protocol handlers. Subclasses should call super.init()
47+
* and
4548
* may register additional handlers for custom protocol extensions.
4649
*/
4750
@Override
@@ -68,8 +71,8 @@ public void init() {
6871
this.registerRpcHandler(Methods.SHUTDOWN, (req, res, app) -> app.handleShutdown(req, res));
6972
this.registerRpcHandler(Methods.GET_STATUS, (req, res, app) -> app.handleGetStatus(req, res));
7073
this.registerRpcHandler(Methods.INITIALIZED_NOTIFICATION, (req, res, app) -> {
71-
if (app.sessionState == SessionState.INITIALIZING) {
72-
app.sessionState = SessionState.READY;
74+
if (app.getSessionState() == SessionState.INITIALIZING) {
75+
app.setSessionState(SessionState.READY);
7376
res.setResult(null);
7477
} else {
7578
res.setError(new JsonRpcError(ErrorCodes.INVALID_REQUEST, "Not in initializing state"));
@@ -81,9 +84,9 @@ public void init() {
8184
if (params != null) {
8285
String requestId = params.get("requestId") != null ? params.get("requestId").toString() : "unknown";
8386
String reason = params.get("reason") != null ? params.get("reason").toString() : "No reason provided";
84-
87+
8588
LOGGER.info("Received cancellation notification for request ID: " + requestId + ", reason: " + reason);
86-
89+
8790
// According to MCP spec, cancellation notifications should not send a response
8891
// They are "fire and forget" notifications
8992
res.setResult(null);
@@ -110,6 +113,14 @@ public String handleRpcRequest(Request request, Response response) throws Applic
110113
// Validate authentication
111114
authHandler.validateAuthHeader(request);
112115

116+
// Session management: extract or assign sessionId
117+
String sessionId = (String) request.headers().get(Header.value0f(Http.SESSION_ID));
118+
if (sessionId == null || sessionId.isEmpty()) {
119+
sessionId = request.getSession().getId();
120+
}
121+
response.addHeader(Http.SESSION_ID, sessionId);
122+
currentSessionId.set(sessionId);
123+
113124
// Parse the JSON-RPC request
114125
String requestBody = request.body();
115126
assert requestBody != null;
@@ -120,14 +131,8 @@ public String handleRpcRequest(Request request, Response response) throws Applic
120131
}
121132

122133
if (requestBody.contains("\"method\":\"initialize\"")) {
123-
// Session management: extract or assign sessionId
124-
String sessionId = (String) request.headers().get(Header.value0f("Mcp-Session-Id"));
125-
if (sessionId == null || sessionId.isEmpty()) {
126-
sessionId = request.getSession().getId();
127-
}
128-
response.addHeader("Mcp-Session-Id", sessionId);
129-
sessionMap.put(sessionId, System.currentTimeMillis()); // Store session state as needed
130-
sessionState = SessionState.INITIALIZING; // Set initial state
134+
sessionMap.put(sessionId, System.currentTimeMillis()); // Store session start time
135+
setSessionState(SessionState.INITIALIZING); // Set initial state
131136
}
132137
// Add batch request support
133138
else if (requestBody.trim().startsWith("[")) {
@@ -136,7 +141,8 @@ else if (requestBody.trim().startsWith("[")) {
136141
if (handler != null) {
137142
handler.handle(rpcReq, rpcRes, this);
138143
} else {
139-
rpcRes.setError(new JsonRpcError(ErrorCodes.METHOD_NOT_FOUND, "Method not found: " + rpcReq.getMethod()));
144+
rpcRes.setError(new JsonRpcError(ErrorCodes.METHOD_NOT_FOUND,
145+
"Method not found: " + rpcReq.getMethod()));
140146
}
141147
});
142148
}
@@ -160,7 +166,10 @@ else if (requestBody.trim().startsWith("[")) {
160166
} catch (Exception e) {
161167
LOGGER.log(Level.SEVERE, "RPC request failed", e);
162168
response.setStatus(ResponseStatus.INTERNAL_SERVER_ERROR);
163-
return jsonRpcHandler.createErrorResponse("Internal server error: " + e.getMessage(), ErrorCodes.INTERNAL_ERROR);
169+
return jsonRpcHandler.createErrorResponse("Internal server error: " + e.getMessage(),
170+
ErrorCodes.INTERNAL_ERROR);
171+
} finally {
172+
currentSessionId.remove();
164173
}
165174
}
166175

@@ -173,7 +182,7 @@ else if (requestBody.trim().startsWith("[")) {
173182
*/
174183
protected void handleInitialize(JsonRpcRequest request, JsonRpcResponse response) {
175184
// Set protocolVersion as required
176-
String protocolVersion = "2024-11-05";
185+
String protocolVersion = PROTOCOL_VERSION;
177186

178187
// Build capabilities object as required by MCP
179188
Builder capabilities = new Builder();
@@ -195,21 +204,21 @@ protected void handleInitialize(JsonRpcRequest request, JsonRpcResponse response
195204

196205
// Build serverInfo with name, title, and version
197206
Builder serverInfo = new Builder()
198-
.put("name", "TinyStructMCP")
199-
.put("title", "TinyStruct MCP Server")
207+
.put("name", "tinystruct-mcp")
208+
.put("title", "tinystruct MCP Server")
200209
.put("version", version());
201210

202211
// Build result object
203212
Builder result = new Builder()
204213
.put("protocolVersion", protocolVersion)
205214
.put("capabilities", capabilities)
206215
.put("serverInfo", serverInfo)
207-
.put("instructions", "Welcome to TinyStruct MCP.");
216+
.put("instructions", "Welcome to tinystruct MCP.");
208217

209218
response.setId(request.getId());
210219
response.setResult(result);
211220
// Set session state to READY
212-
sessionState = SessionState.READY;
221+
setSessionState(SessionState.READY);
213222
}
214223

215224
/**
@@ -242,12 +251,12 @@ protected void handleGetCapabilities(JsonRpcRequest request, JsonRpcResponse res
242251
* @param response The JSON-RPC response to populate
243252
*/
244253
protected void handleShutdown(JsonRpcRequest request, JsonRpcResponse response) {
245-
if (sessionState != SessionState.READY) {
254+
if (getSessionState() != SessionState.READY) {
246255
response.setError(new JsonRpcError(ErrorCodes.NOT_INITIALIZED, "Not in ready state"));
247256
return;
248257
}
249258

250-
sessionState = SessionState.DISCONNECTED;
259+
setSessionState(SessionState.DISCONNECTED);
251260

252261
response.setId(request.getId());
253262
response.setResult(new Builder().put("status", "shutdown_complete"));
@@ -280,7 +289,7 @@ protected void handleGetStatus(JsonRpcRequest request, JsonRpcResponse response)
280289
response.setError(new JsonRpcError(ErrorCodes.INTERNAL_ERROR, "Session start time invalid"));
281290
return;
282291
}
283-
result.put("state", sessionState.toString());
292+
result.put("state", getSessionState().toString());
284293
result.put("sessionId", sessionId);
285294
result.put("uptime", System.currentTimeMillis() - sessionStart);
286295
response.setId(request.getId());
@@ -314,26 +323,16 @@ protected String[] getFeatures() {
314323
* @param tool The tool to register
315324
*/
316325
public void registerTool(MCPTool tool) {
317-
Builder builder = SchemaGenerator.generateSchema(tool.getClass());
326+
Class<? extends MCPTool> toolClass = tool.getClass();
327+
Builder builder = SchemaGenerator.generateSchema(toolClass);
318328
tool.setSchema(builder);
319329
tools.put(tool.getName(), tool);
320330
LOGGER.info("Registered tool: " + tool.getName());
321-
}
322-
323-
/**
324-
* Registers a tool class and extracts all its methods as individual tools.
325-
* This method scans the tool class for methods annotated with @Action and
326-
* registers each method as a separate tool method.
327-
*
328-
* @param toolInstance The tool instance to register
329-
*/
330-
public void registerToolMethods(Object toolInstance) {
331-
Class<?> toolClass = toolInstance.getClass();
332331

333332
for (Method method : toolClass.getDeclaredMethods()) {
334333
Action action = method.getAnnotation(Action.class);
335334
if (action != null) {
336-
MCPTool.MCPToolMethod toolMethod = new MCPTool.MCPToolMethod(method, action, toolInstance);
335+
MCPTool.MCPToolMethod toolMethod = new MCPTool.MCPToolMethod(method, action, tool);
337336
toolMethods.put(toolMethod.getName(), toolMethod);
338337
LOGGER.info("Registered tool method: " + toolMethod.getName());
339338
}
@@ -427,4 +426,28 @@ protected void registerRpcHandler(String method, RpcMethodHandler handler) {
427426
*/
428427
abstract void handleGetPrompt(JsonRpcRequest request, JsonRpcResponse response);
429428

429+
/**
430+
* Gets the current session state based on the current session ID.
431+
*
432+
* @return The current state
433+
*/
434+
protected SessionState getSessionState() {
435+
String sessionId = currentSessionId.get();
436+
if (sessionId != null) {
437+
return sessionStates.getOrDefault(sessionId, SessionState.DISCONNECTED);
438+
}
439+
return SessionState.DISCONNECTED;
440+
}
441+
442+
/**
443+
* Sets the session state for the current session.
444+
*
445+
* @param state The new state
446+
*/
447+
protected void setSessionState(SessionState state) {
448+
String sessionId = currentSessionId.get();
449+
if (sessionId != null) {
450+
sessionStates.put(sessionId, state);
451+
}
452+
}
430453
}

0 commit comments

Comments
 (0)